PageSourceSearch

https://www.bhpanel.org/wp-content/themes/dailypost/js/vendors/medium.js?ver=7.1.3

js bhpanel.org collected 2026-10-06 19:14:28 UTC 252,722 bytes, 6,445 lines download raw bytes

vendor: 11,165 bytes, lines 1-388
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    
vendor: 10,256 bytes, lines 388-638
388if (typeof module === 'object') {
389        module.exports = factory;
390    } else if (typeof define === 'function' && define.amd) {
391        define(function () {
392            return factory;
393        });
394    } else {
395        root.MediumEditor = factory;
396    }
397}(this, function () {
398
399    'use strict';
400
401function MediumEditor(elements, options) {
402    'use strict';
403    return this.init(elements, options);
404}
405
406MediumEditor.extensions = {};
407/*jshint unused: true */
408(function (window) {
409    'use strict';
410
411    function copyInto(overwrite, dest) {
412        var prop,
413            sources = Array.prototype.slice.call(arguments, 2);
414        dest = dest || {};
415        for (var i = 0; i < sources.length; i++) {
416            var source = sources[i];
417            if (source) {
418                for (prop in source) {
419                    if (source.hasOwnProperty(prop) &&
420                        typeof source[prop] !== 'undefined' &&
421                        (overwrite || dest.hasOwnProperty(prop) === false)) {
422                        dest[prop] = source[prop];
423                    }
424                }
425            }
426        }
427        return dest;
428    }
429
430    // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
431    // Some browsers (including phantom) don't return true for Node.contains(child)
432    // if child is a text node.  Detect these cases here and use a fallback
433    // for calls to Util.isDescendant()
434    var nodeContainsWorksWithTextNodes = false;
435    try {
436        var testParent = document.createElement('div'),
437            testText = document.createTextNode(' ');
438        testParent.appendChild(testText);
439        nodeContainsWorksWithTextNodes = testParent.contains(testText);
440    } catch (exc) {}
441
442    var Util = {
443
444        // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
445        // by rg89
446        isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
447
448        // http://stackoverflow.com/a/11752084/569101
449        isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0),
450
451        // https://github.com/jashkenas/underscore
452        keyCode: {
453            BACKSPACE: 8,
454            TAB: 9,
455            ENTER: 13,
456            ESCAPE: 27,
457            SPACE: 32,
458            DELETE: 46,
459            K: 75, // K keycode, and not k
460            M: 77
461        },
462
463        /**
464         * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac.
465         * See #591
466         */
467        isMetaCtrlKey: function (event) {
468            if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) {
469                return true;
470            }
471
472            return false;
473        },
474
475        /**
476         * Returns true if the key associated to the event is inside keys array
477         *
478         * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484
479         * @see : http://stackoverflow.com/q/4471582/569101
480         */
481        isKey: function (event, keys) {
482            var keyCode = Util.getKeyCode(event);
483
484            // it's not an array let's just compare strings!
485            if (false === Array.isArray(keys)) {
486                return keyCode === keys;
487            }
488
489            if (-1 === keys.indexOf(keyCode)) {
490                return false;
491            }
492
493            return true;
494        },
495
496        getKeyCode: function (event) {
497            var keyCode = event.which;
498
499            // getting the key code from event
500            if (null === keyCode) {
501                keyCode = event.charCode !== null ? event.charCode : event.keyCode;
502            }
503
504            return keyCode;
505        },
506
507        blockContainerElementNames: [
508            // elements our editor generates
509            'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
510            // all other known block elements
511            'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset',
512            'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav',
513            'noscript', 'output', 'section', 'video',
514            'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'
515        ],
516
517        emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'],
518
519        extend: function extend(/* dest, source1, source2, ...*/) {
520            var args = [true].concat(Array.prototype.slice.call(arguments));
521            return copyInto.apply(this, args);
522        },
523
524        defaults: function defaults(/*dest, source1, source2, ...*/) {
525            var args = [false].concat(Array.prototype.slice.call(arguments));
526            return copyInto.apply(this, args);
527        },
528
529        /*
530         * Create a link around the provided text nodes which must be adjacent to each other and all be
531         * descendants of the same closest block container. If the preconditions are not met, unexpected
532         * behavior will result.
533         */
534        createLink: function (document, textNodes, href, target) {
535            var anchor = document.createElement('a');
536            Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor);
537            anchor.setAttribute('href', href);
538            if (target) {
539                anchor.setAttribute('target', target);
540            }
541            return anchor;
542        },
543
544        /*
545         * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the
546         * textContent of the provided element argument, modify the DOM inside element to ensure that the text
547         * identified by the provided match can be returned as text nodes that contain exactly that text, without
548         * any additional text at the beginning or end of the returned array of adjacent text nodes.
549         *
550         * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are
551         * not affected in any way.
552         */
553        findOrCreateMatchingTextNodes: function (document, element, match) {
554            var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),
555                matchedNodes = [],
556                currentTextIndex = 0,
557                startReached = false,
558                currentNode = null,
559                newNode = null;
560
561            while ((currentNode = treeWalker.nextNode()) !== null) {
562                if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) {
563                    startReached = true;
564                    newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex);
565                }
566                if (startReached) {
567                    Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex);
568                }
569                if (startReached && currentTextIndex === match.end) {
570                    break; // Found the node(s) corresponding to the link. Break out and move on to the next.
571                } else if (startReached && currentTextIndex > (match.end + 1)) {
572                    throw new Error('PerformLinking overshot the target!'); // should never happen...
573                }
574
575                if (startReached) {
576                    matchedNodes.push(newNode || currentNode);
577                }
578
579                currentTextIndex += currentNode.nodeValue.length;
580                if (newNode !== null) {
581                    currentTextIndex += newNode.nodeValue.length;
582                    // Skip the newNode as we'll already have pushed it to the matches
583                    treeWalker.nextNode();
584                }
585                newNode = null;
586            }
587            return matchedNodes;
588        },
589
590        /*
591         * Given the provided text node and text coordinates, split the text node if needed to make it align
592         * precisely with the coordinates.
593         *
594         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.
595         */
596        splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) {
597            if (matchStartIndex !== currentTextIndex) {
598                return currentNode.splitText(matchStartIndex - currentTextIndex);
599            }
600            return null;
601        },
602
603        /*
604         * Given the provided text node and text coordinates, split the text node if needed to make it align
605         * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded,
606         * if that function has been called on the same currentNode.
607         *
608         * This function is intended to be called from Util.findOrCreateMatchingTextNodes.
609         */
610        splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) {
611            var textIndexOfEndOfFarthestNode,
612                endSplitPoint;
613            textIndexOfEndOfFarthestNode = currentTextIndex + (newNode || currentNode).nodeValue.length +
614                    (newNode ? currentNode.nodeValue.length : 0) -
615                    1;
616            endSplitPoint = (newNode || currentNode).nodeValue.length -
617                    (textIndexOfEndOfFarthestNode + 1 - matchEndIndex);
618            if (textIndexOfEndOfFarthestNode >= matchEndIndex &&
619                    currentTextIndex !== textIndexOfEndOfFarthestNode &&
620                    endSplitPoint !== 0) {
621                (newNode || currentNode).splitText(endSplitPoint);
622            }
623        },
624
625        /*
626        * Take an element, and break up all of its text content into unique pieces such that:
627         * 1) All text content of the elements are in separate blocks. No piece of text content should span
628         *    span multiple blocks. This means no element return by this function should have
629         *    any blocks as children.
630         * 2) The union of the textcontent of all of the elements returned here covers all
631         *    of the text within the element.
632         *
633         *
634         * EXAMPLE:
635         * In the event that we have something like:
636         *
637         * <blockquote>
638         *   <p>Some Text</p>
639         *   <ol>
640         *     <li>List Item 1</li>
641         *     <li>List Item 2</li>
642         *   </ol>
643         * </blockquote>
644         *
645         * This function would return these elements as an array:
646         *   [ <p>Some Text</p>, <li>List Item 1</li>, <li>List Item 2</li> ]
647         *
648         * Since the <blockquote> and <ol> elements contain blocks within them they are not returned.
649         * Since the <p> and <li>'s don't contain block elements and cover all the text content of the
650         * <blockquote> container, they are the elements returned.
651         */
652        splitByBlockElements: function (element) {
653            var toRet = [],
654                blockElementQuery = MediumEditor.util.blockContainerElementNames.join(',');
655
656            if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) {
657                return [element];
658            }
659
660            for (var i = 0; i < element.childNodes.length; i++) {
661                var child = element.childNodes[i];
662                if (child.nodeType === 3) {
663                    toRet.push(child);
664                } else {
665                    var blockElements = child.querySelectorAll(blockElementQuery);
666                    if (blockElements.length === 0) {
667                        toRet.push(child);
668                    } else {
669                        toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child));
670                    }
671                }
672            }
673
674            return toRet;
675        },
676
677        // Find the next node in the DOM tree that represents any text that is being
678        // displayed directly next to the targetNode (passed as an argument)
679        // Text that appears directly next to the current node can be:
680        //  - A sibling text node
681        //  - A descendant of a sibling element
682        //  - A sibling text node of an ancestor
683        //  - A descendant of a sibling element of an ancestor
684        findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
685            var pastTarget = false,
686                nextNode,
687                nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
688
689            // Use a native NodeIterator to iterate over all the text nodes that are descendants
690            // of the rootNode.  Once past the targetNode, choose the first non-empty text node
691            nextNode = nodeIterator.nextNode();
692            while (nextNode) {
693                if (nextNode === targetNode) {
694                    pastTarget = true;
695                } else if (pastTarget) {
696                    if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
697                        break;
698                    }
699                }
700                nextNode = nodeIterator.nextNode();
701            }
702
703            return nextNode;
704        },
705
706        isDescendant: function isDescendant(parent, child, checkEquality) {
707            if (!parent || !child) {
708                return false;
709            }
710            if (parent === child) {
711                return !!checkEquality;
712            }
713            // If parent is not an element, it can't have any descendants
714            if (parent.nodeType !== 1) {
715                return false;
716            }
717            if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) {
718                return parent.contains(child);
719            }
720            var node = child.parentNode;
721            while (node !== null) {
722                if (node === parent) {
723                    return true;
724                }
725                node = node.parentNode;
726            }
727            return false;
728        },
729
730        // https://github.com/jashkenas/underscore
731        isElement: function isElement(obj) {
732            return !!(obj && obj.nodeType === 1);
733        },
734
735        // https://github.com/jashkenas/underscore
736        throttle: function (func, wait) {
737            var THROTTLE_INTERVAL = 50,
738                context,
739                args,
740                result,
741                timeout = null,
742                previous = 0,
743                later = function () {
744                    previous = Date.now();
745                    timeout = null;
vendor: 4,303 bytes, lines 746-847
746                    result = func.apply(context, args);
747                    if (!timeout) {
748                        context = args = null;
749                    }
750                };
751
752            if (!wait && wait !== 0) {
753                wait = THROTTLE_INTERVAL;
754            }
755
756            return function () {
757                var now = Date.now(),
758                    remaining = wait - (now - previous);
759
760                context = this;
761                args = arguments;
762                if (remaining <= 0 || remaining > wait) {
763                    if (timeout) {
764                        clearTimeout(timeout);
765                        timeout = null;
766                    }
767                    previous = now;
768                    result = func.apply(context, args);
769                    if (!timeout) {
770                        context = args = null;
771                    }
772                } else if (!timeout) {
773                    timeout = setTimeout(later, remaining);
774                }
775                return result;
776            };
777        },
778
779        traverseUp: function (current, testElementFunction) {
780            if (!current) {
781                return false;
782            }
783
784            do {
785                if (current.nodeType === 1) {
786                    if (testElementFunction(current)) {
787                        return current;
788                    }
789                    // do not traverse upwards past the nearest containing editor
790                    if (Util.isMediumEditorElement(current)) {
791                        return false;
792                    }
793                }
794
795                current = current.parentNode;
796            } while (current);
797
798            return false;
799        },
800
801        htmlEntities: function (str) {
802            // converts special characters (like <) into their escaped/encoded values (like &lt;).
803            // This allows you to show to display the string without the browser reading it as HTML.
804            return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
805        },
806
807        // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
808        insertHTMLCommand: function (doc, html) {
809            var selection, range, el, fragment, node, lastNode, toReplace;
810
811            if (doc.queryCommandSupported('insertHTML')) {
812                try {
813                    return doc.execCommand('insertHTML', false, html);
814                } catch (ignore) {}
815            }
816
817            selection = doc.getSelection();
818            if (selection.rangeCount) {
819                range = selection.getRangeAt(0);
820                toReplace = range.commonAncestorContainer;
821
822                // https://github.com/yabwe/medium-editor/issues/748
823                // If the selection is an empty editor element, create a temporary text node inside of the editor
824                // and select it so that we don't delete the editor element
825                if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) {
826                    range.selectNode(toReplace.appendChild(doc.createTextNode('')));
827                } else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) ||
828                        (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) {
829                    // Ensure range covers maximum amount of nodes as possible
830                    // By moving up the DOM and selecting ancestors whose only child is the range
831                    while (!Util.isMediumEditorElement(toReplace) &&
832                            toReplace.parentNode &&
833                            toReplace.parentNode.childNodes.length === 1 &&
834                            !Util.isMediumEditorElement(toReplace.parentNode)) {
835                        toReplace = toReplace.parentNode;
836                    }
837                    range.selectNode(toReplace);
838                }
839                range.deleteContents();
840
841                el = doc.createElement('div');
842                el.innerHTML = html;
843                fragment = doc.createDocumentFragment();
844                while (el.firstChild) {
845                    node = el.firstChild;
846                    lastNode = fragment.appendChild(node);
847                }
848                range.insertNode(fragment);
849
850                // Preserve the selection:
851                if (lastNode) {
852                    range = range.cloneRange();
853                    range.setStartAfter(lastNode);
854                    range.collapse(true);
855                    selection.removeAllRanges();
856                    selection.addRange(range);
857                }
858            }
859        },
860
861        execFormatBlock: function (doc, tagName) {
862            // Get the top level block element that contains the selection
863            var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc));
864
865            // Special handling for blockquote
866            if (tagName === 'blockquote') {
867                if (blockContainer) {
868                    var childNodes = Array.prototype.slice.call(blockContainer.childNodes);
869                    // Check if the blockquote has a block element as a child (nested blocks)
870                    if (childNodes.some(function (childNode) {
871                        return Util.isBlockContainer(childNode);
872                    })) {
873                        // FF handles blockquote differently on formatBlock
874                        // allowing nesting, we need to use outdent
875                        // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
876                        return doc.execCommand('outdent', false, null);
877                    }
878                }
879
880                // When IE blockquote needs to be called as indent
881                // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
882                if (Util.isIE) {
883                    return doc.execCommand('indent', false, tagName);
884                }
885            }
886
887            // If the blockContainer is already the element type being passed in
888            // treat it as 'undo' formatting and just convert it to a <p>
889            if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) {
890                tagName = 'p';
891            }
892
893            // When IE we need to add <> to heading elements
894            // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
895            if (Util.isIE) {
896                tagName = '<' + tagName + '>';
897            }
898            return doc.execCommand('formatBlock', false, tagName);
899        },
900
901        /**
902         * Set target to blank on the given el element
903         *
904         * TODO: not sure if this should be here
905         *
906         * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link
907         * 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
908         * using `anchorUrl` to ensure that we are adding target="_blank" on the good one.
909         * This isn't a bulletproof solution anyway ..
910         */
911        setTargetBlank: function (el, anchorUrl) {
912            var i, url = anchorUrl || false;
913            if (el.nodeName.toLowerCase() === 'a') {
914                el.target = '_blank';
915            } else {
916                el = el.getElementsByTagName('a');
917
918                for (i = 0; i < el.length; i += 1) {
919                    if (false === url || url === el[i].attributes.href.value) {
920                        el[i].target = '_blank';
921                    }
922                }
923            }
924        },
925
926        addClassToAnchors: function (el, buttonClass) {
927            var classes = buttonClass.split(' '),
928                i,
929                j;
930            if (el.nodeName.toLowerCase() === 'a') {
931                for (j = 0; j < classes.length; j += 1) {
932                    el.classList.add(classes[j]);
933                }
934            } else {
935                el = el.getElementsByTagName('a');
936                for (i = 0; i < el.length; i += 1) {
937                    for (j = 0; j < classes.length; j += 1) {
938                        el[i].classList.add(classes[j]);
939                    }
940                }
941            }
942        },
943
944        isListItem: function (node) {
945            if (!node) {
946                return false;
vendor: 50,775 bytes, lines 947-2260
947            }
948            if (node.nodeName.toLowerCase() === 'li') {
949                return true;
950            }
951
952            var parentNode = node.parentNode,
953                tagName = parentNode.nodeName.toLowerCase();
954            while (tagName === 'li' || (!Util.isBlockContainer(parentNode) && tagName !== 'div')) {
955                if (tagName === 'li') {
956                    return true;
957                }
958                parentNode = parentNode.parentNode;
959                if (parentNode) {
960                    tagName = parentNode.nodeName.toLowerCase();
961                } else {
962                    return false;
963                }
964            }
965            return false;
966        },
967
968        cleanListDOM: function (ownerDocument, element) {
969            if (element.nodeName.toLowerCase() !== 'li') {
970                return;
971            }
972
973            var list = element.parentElement;
974
975            if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up
976                Util.unwrap(list.parentElement, ownerDocument);
977
978                // move cursor at the end of the text inside the list
979                // for some unknown reason, the cursor is moved to end of the "visual" line
980                MediumEditor.selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length);
981            }
982        },
983
984        /* splitDOMTree
985         *
986         * Given a root element some descendant element, split the root element
987         * into its own element containing the descendant element and all elements
988         * on the left or right side of the descendant ('right' is default)
989         *
990         * example:
991         *
992         *         <div>
993         *      /    |   \
994         *  <span> <span> <span>
995         *   / \    / \    / \
996         *  1   2  3   4  5   6
997         *
998         *  If I wanted to split this tree given the <div> as the root and "4" as the leaf
999         *  the result would be (the prime ' marks indicates nodes that are created as clones):
1000         *
1001         *   SPLITTING OFF 'RIGHT' TREE       SPLITTING OFF 'LEFT' TREE
1002         *
1003         *     <div>            <div>'              <div>'      <div>
1004         *      / \              / \                 / \          |
1005         * <span> <span>   <span>' <span>       <span> <span>   <span>
1006         *   / \    |        |      / \           /\     /\       /\
1007         *  1   2   3        4     5   6         1  2   3  4     5  6
1008         *
1009         *  The above example represents splitting off the 'right' or 'left' part of a tree, where
1010         *  the <div>' would be returned as an element not appended to the DOM, and the <div>
1011         *  would remain in place where it was
1012         *
1013        */
1014        splitOffDOMTree: function (rootNode, leafNode, splitLeft) {
1015            var splitOnNode = leafNode,
1016                createdNode = null,
1017                splitRight = !splitLeft;
1018
1019            // loop until we hit the root
1020            while (splitOnNode !== rootNode) {
1021                var currParent = splitOnNode.parentNode,
1022                    newParent = currParent.cloneNode(false),
1023                    targetNode = (splitRight ? splitOnNode : currParent.firstChild),
1024                    appendLast;
1025
1026                // Create a new parent element which is a clone of the current parent
1027                if (createdNode) {
1028                    if (splitRight) {
1029                        // If we're splitting right, add previous created element before siblings
1030                        newParent.appendChild(createdNode);
1031                    } else {
1032                        // If we're splitting left, add previous created element last
1033                        appendLast = createdNode;
1034                    }
1035                }
1036                createdNode = newParent;
1037
1038                while (targetNode) {
1039                    var sibling = targetNode.nextSibling;
1040                    // Special handling for the 'splitNode'
1041                    if (targetNode === splitOnNode) {
1042                        if (!targetNode.hasChildNodes()) {
1043                            targetNode.parentNode.removeChild(targetNode);
1044                        } else {
1045                            // For the node we're splitting on, if it has children, we need to clone it
1046                            // and not just move it
1047                            targetNode = targetNode.cloneNode(false);
1048                        }
1049                        // If the resulting split node has content, add it
1050                        if (targetNode.textContent) {
1051                            createdNode.appendChild(targetNode);
1052                        }
1053
1054                        targetNode = (splitRight ? sibling : null);
1055                    } else {
1056                        // For general case, just remove the element and only
1057                        // add it to the split tree if it contains something
1058                        targetNode.parentNode.removeChild(targetNode);
1059                        if (targetNode.hasChildNodes() || targetNode.textContent) {
1060                            createdNode.appendChild(targetNode);
1061                        }
1062
1063                        targetNode = sibling;
1064                    }
1065                }
1066
1067                // If we had an element we wanted to append at the end, do that now
1068                if (appendLast) {
1069                    createdNode.appendChild(appendLast);
1070                }
1071
1072                splitOnNode = currParent;
1073            }
1074
1075            return createdNode;
1076        },
1077
1078        moveTextRangeIntoElement: function (startNode, endNode, newElement) {
1079            if (!startNode || !endNode) {
1080                return false;
1081            }
1082
1083            var rootNode = Util.findCommonRoot(startNode, endNode);
1084            if (!rootNode) {
1085                return false;
1086            }
1087
1088            if (endNode === startNode) {
1089                var temp = startNode.parentNode,
1090                    sibling = startNode.nextSibling;
1091                temp.removeChild(startNode);
1092                newElement.appendChild(startNode);
1093                if (sibling) {
1094                    temp.insertBefore(newElement, sibling);
1095                } else {
1096                    temp.appendChild(newElement);
1097                }
1098                return newElement.hasChildNodes();
1099            }
1100
1101            // create rootChildren array which includes all the children
1102            // we care about
1103            var rootChildren = [],
1104                firstChild,
1105                lastChild,
1106                nextNode;
1107            for (var i = 0; i < rootNode.childNodes.length; i++) {
1108                nextNode = rootNode.childNodes[i];
1109                if (!firstChild) {
1110                    if (Util.isDescendant(nextNode, startNode, true)) {
1111                        firstChild = nextNode;
1112                    }
1113                } else {
1114                    if (Util.isDescendant(nextNode, endNode, true)) {
1115                        lastChild = nextNode;
1116                        break;
1117                    } else {
1118                        rootChildren.push(nextNode);
1119                    }
1120                }
1121            }
1122
1123            var afterLast = lastChild.nextSibling,
1124                fragment = rootNode.ownerDocument.createDocumentFragment();
1125
1126            // build up fragment on startNode side of tree
1127            if (firstChild === startNode) {
1128                firstChild.parentNode.removeChild(firstChild);
1129                fragment.appendChild(firstChild);
1130            } else {
1131                fragment.appendChild(Util.splitOffDOMTree(firstChild, startNode));
1132            }
1133
1134            // add any elements between firstChild & lastChild
1135            rootChildren.forEach(function (element) {
1136                element.parentNode.removeChild(element);
1137                fragment.appendChild(element);
1138            });
1139
1140            // build up fragment on endNode side of the tree
1141            if (lastChild === endNode) {
1142                lastChild.parentNode.removeChild(lastChild);
1143                fragment.appendChild(lastChild);
1144            } else {
1145                fragment.appendChild(Util.splitOffDOMTree(lastChild, endNode, true));
1146            }
1147
1148            // Add fragment into passed in element
1149            newElement.appendChild(fragment);
1150
1151            if (lastChild.parentNode === rootNode) {
1152                // If last child is in the root, insert newElement in front of it
1153                rootNode.insertBefore(newElement, lastChild);
1154            } else if (afterLast) {
1155                // If last child was removed, but it had a sibling, insert in front of it
1156                rootNode.insertBefore(newElement, afterLast);
1157            } else {
1158                // lastChild was removed and was the last actual element just append
1159                rootNode.appendChild(newElement);
1160            }
1161
1162            return newElement.hasChildNodes();
1163        },
1164
1165        /* based on http://stackoverflow.com/a/6183069 */
1166        depthOfNode: function (inNode) {
1167            var theDepth = 0,
1168                node = inNode;
1169            while (node.parentNode !== null) {
1170                node = node.parentNode;
1171                theDepth++;
1172            }
1173            return theDepth;
1174        },
1175
1176        findCommonRoot: function (inNode1, inNode2) {
1177            var depth1 = Util.depthOfNode(inNode1),
1178                depth2 = Util.depthOfNode(inNode2),
1179                node1 = inNode1,
1180                node2 = inNode2;
1181
1182            while (depth1 !== depth2) {
1183                if (depth1 > depth2) {
1184                    node1 = node1.parentNode;
1185                    depth1 -= 1;
1186                } else {
1187                    node2 = node2.parentNode;
1188                    depth2 -= 1;
1189                }
1190            }
1191
1192            while (node1 !== node2) {
1193                node1 = node1.parentNode;
1194                node2 = node2.parentNode;
1195            }
1196
1197            return node1;
1198        },
1199        /* END - based on http://stackoverflow.com/a/6183069 */
1200
1201        isElementAtBeginningOfBlock: function (node) {
1202            var textVal,
1203                sibling;
1204            while (!Util.isBlockContainer(node) && !Util.isMediumEditorElement(node)) {
1205                sibling = node;
1206                while (sibling = sibling.previousSibling) {
1207                    textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent;
1208                    if (textVal.length > 0) {
1209                        return false;
1210                    }
1211                }
1212                node = node.parentNode;
1213            }
1214            return true;
1215        },
1216
1217        isMediumEditorElement: function (element) {
1218            return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element');
1219        },
1220
1221        getContainerEditorElement: function (element) {
1222            return Util.traverseUp(element, function (node) {
1223                return Util.isMediumEditorElement(node);
1224            });
1225        },
1226
1227        isBlockContainer: function (element) {
1228            return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1;
1229        },
1230
1231        getClosestBlockContainer: function (node) {
1232            return Util.traverseUp(node, function (node) {
1233                return Util.isBlockContainer(node);
1234            });
1235        },
1236
1237        getTopBlockContainer: function (element) {
1238            var topBlock = element;
1239            Util.traverseUp(element, function (el) {
1240                if (Util.isBlockContainer(el)) {
1241                    topBlock = el;
1242                }
1243                return false;
1244            });
1245            return topBlock;
1246        },
1247
1248        getFirstSelectableLeafNode: function (element) {
1249            while (element && element.firstChild) {
1250                element = element.firstChild;
1251            }
1252
1253            // We don't want to set the selection to an element that can't have children, this messes up Gecko.
1254            element = Util.traverseUp(element, function (el) {
1255                return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1;
1256            });
1257            // Selecting at the beginning of a table doesn't work in PhantomJS.
1258            if (element.nodeName.toLowerCase() === 'table') {
1259                var firstCell = element.querySelector('th, td');
1260                if (firstCell) {
1261                    element = firstCell;
1262                }
1263            }
1264            return element;
1265        },
1266
1267        getFirstTextNode: function (element) {
1268            if (element.nodeType === 3) {
1269                return element;
1270            }
1271
1272            for (var i = 0; i < element.childNodes.length; i++) {
1273                var textNode = Util.getFirstTextNode(element.childNodes[i]);
1274                if (textNode !== null) {
1275                    return textNode;
1276                }
1277            }
1278            return null;
1279        },
1280
1281        ensureUrlHasProtocol: function (url) {
1282            if (url.indexOf('://') === -1) {
1283                return 'http://' + url;
1284            }
1285            return url;
1286        },
1287
1288        warn: function () {
1289            if (window.console !== undefined && typeof window.console.warn === 'function') {
1290                window.console.warn.apply(window.console, arguments);
1291            }
1292        },
1293
1294        deprecated: function (oldName, newName, version) {
1295            // simple deprecation warning mechanism.
1296            var m = oldName + ' is deprecated, please use ' + newName + ' instead.';
1297            if (version) {
1298                m += ' Will be removed in ' + version;
1299            }
1300            Util.warn(m);
1301        },
1302
1303        deprecatedMethod: function (oldName, newName, args, version) {
1304            // run the replacement and warn when someone calls a deprecated method
1305            Util.deprecated(oldName, newName, version);
1306            if (typeof this[newName] === 'function') {
1307                this[newName].apply(this, args);
1308            }
1309        },
1310
1311        cleanupAttrs: function (el, attrs) {
1312            attrs.forEach(function (attr) {
1313                el.removeAttribute(attr);
1314            });
1315        },
1316
1317        cleanupTags: function (el, tags) {
1318            tags.forEach(function (tag) {
1319                if (el.nodeName.toLowerCase() === tag) {
1320                    el.parentNode.removeChild(el);
1321                }
1322            });
1323        },
1324
1325        // get the closest parent
1326        getClosestTag: function (el, tag) {
1327            return Util.traverseUp(el, function (element) {
1328                return element.nodeName.toLowerCase() === tag.toLowerCase();
1329            });
1330        },
1331
1332        unwrap: function (el, doc) {
1333            var fragment = doc.createDocumentFragment(),
1334                nodes = Array.prototype.slice.call(el.childNodes);
1335
1336            // cast nodeList to array since appending child
1337            // to a different node will alter length of el.childNodes
1338            for (var i = 0; i < nodes.length; i++) {
1339                fragment.appendChild(nodes[i]);
1340            }
1341
1342            if (fragment.childNodes.length) {
1343                el.parentNode.replaceChild(fragment, el);
1344            } else {
1345                el.parentNode.removeChild(el);
1346            }
1347        }
1348    };
1349
1350    MediumEditor.util = Util;
1351}(window));
1352
1353(function () {
1354    'use strict';
1355
1356    var Extension = function (options) {
1357        MediumEditor.util.extend(this, options);
1358    };
1359
1360    Extension.extend = function (protoProps) {
1361        // magic extender thinger. mostly borrowed from backbone/goog.inherits
1362        // place this function on some thing you want extend-able.
1363        //
1364        // example:
1365        //
1366        //      function Thing(args){
1367        //          this.options = args;
1368        //      }
1369        //
1370        //      Thing.prototype = { foo: "bar" };
1371        //      Thing.extend = extenderify;
1372        //
1373        //      var ThingTwo = Thing.extend({ foo: "baz" });
1374        //
1375        //      var thingOne = new Thing(); // foo === "bar"
1376        //      var thingTwo = new ThingTwo(); // foo === "baz"
1377        //
1378        //      which seems like some simply shallow copy nonsense
1379        //      at first, but a lot more is going on there.
1380        //
1381        //      passing a `constructor` to the extend props
1382        //      will cause the instance to instantiate through that
1383        //      instead of the parent's constructor.
1384
1385        var parent = this,
1386            child;
1387
1388        // The constructor function for the new subclass is either defined by you
1389        // (the "constructor" property in your `extend` definition), or defaulted
1390        // by us to simply call the parent's constructor.
1391
1392        if (protoProps && protoProps.hasOwnProperty('constructor')) {
1393            child = protoProps.constructor;
1394        } else {
1395            child = function () {
1396                return parent.apply(this, arguments);
1397            };
1398        }
1399
1400        // das statics (.extend comes over, so your subclass can have subclasses too)
1401        MediumEditor.util.extend(child, parent);
1402
1403        // Set the prototype chain to inherit from `parent`, without calling
1404        // `parent`'s constructor function.
1405        var Surrogate = function () {
1406            this.constructor = child;
1407        };
1408        Surrogate.prototype = parent.prototype;
1409        child.prototype = new Surrogate();
1410
1411        if (protoProps) {
1412            MediumEditor.util.extend(child.prototype, protoProps);
1413        }
1414
1415        // todo: $super?
1416
1417        return child;
1418    };
1419
1420    Extension.prototype = {
1421        /* init: [function]
1422         *
1423         * Called by MediumEditor during initialization.
1424         * The .base property will already have been set to
1425         * current instance of MediumEditor when this is called.
1426         * All helper methods will exist as well
1427         */
1428        init: function () {},
1429
1430        /* base: [MediumEditor instance]
1431         *
1432         * If not overriden, this will be set to the current instance
1433         * of MediumEditor, before the init method is called
1434         */
1435        base: undefined,
1436
1437        /* name: [string]
1438         *
1439         * 'name' of the extension, used for retrieving the extension.
1440         * If not set, MediumEditor will set this to be the key
1441         * used when passing the extension into MediumEditor via the
1442         * 'extensions' option
1443         */
1444        name: undefined,
1445
1446        /* checkState: [function (node)]
1447         *
1448         * If implemented, this function will be called one or more times
1449         * the state of the editor & toolbar are updated.
1450         * When the state is updated, the editor does the following:
1451         *
1452         * 1) Find the parent node containing the current selection
1453         * 2) Call checkState on the extension, passing the node as an argument
1454         * 3) Get the parent node of the previous node
1455         * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable
1456         */
1457        checkState: undefined,
1458
1459        /* destroy: [function ()]
1460         *
1461         * This method should remove any created html, custom event handlers
1462         * or any other cleanup tasks that should be performed.
1463         * If implemented, this function will be called when MediumEditor's
1464         * destroy method has been called.
1465         */
1466        destroy: undefined,
1467
1468        /* As alternatives to checkState, these functions provide a more structured
1469         * path to updating the state of an extension (usually a button) whenever
1470         * the state of the editor & toolbar are updated.
1471         */
1472
1473        /* queryCommandState: [function ()]
1474         *
1475         * If implemented, this function will be called once on each extension
1476         * when the state of the editor/toolbar is being updated.
1477         *
1478         * If this function returns a non-null value, the extension will
1479         * be ignored as the code climbs the dom tree.
1480         *
1481         * If this function returns true, and the setActive() function is defined
1482         * setActive() will be called
1483         */
1484        queryCommandState: undefined,
1485
1486        /* isActive: [function ()]
1487         *
1488         * If implemented, this function will be called when MediumEditor
1489         * has determined that this extension is 'active' for the current selection.
1490         * This may be called when the editor & toolbar are being updated,
1491         * but only if queryCommandState() or isAlreadyApplied() functions
1492         * are implemented, and when called, return true.
1493         */
1494        isActive: undefined,
1495
1496        /* isAlreadyApplied: [function (node)]
1497         *
1498         * If implemented, this function is similar to checkState() in
1499         * that it will be called repeatedly as MediumEditor moves up
1500         * the DOM to update the editor & toolbar after a state change.
1501         *
1502         * NOTE: This function will NOT be called if checkState() has
1503         * been implemented. This function will NOT be called if
1504         * queryCommandState() is implemented and returns a non-null
1505         * value when called
1506         */
1507        isAlreadyApplied: undefined,
1508
1509        /* setActive: [function ()]
1510         *
1511         * If implemented, this function is called when MediumEditor knows
1512         * that this extension is currently enabled.  Currently, this
1513         * function is called when updating the editor & toolbar, and
1514         * only if queryCommandState() or isAlreadyApplied(node) return
1515         * true when called
1516         */
1517        setActive: undefined,
1518
1519        /* setInactive: [function ()]
1520         *
1521         * If implemented, this function is called when MediumEditor knows
1522         * that this extension is currently disabled.  Curently, this
1523         * is called at the beginning of each state change for
1524         * the editor & toolbar. After calling this, MediumEditor
1525         * will attempt to update the extension, either via checkState()
1526         * or the combination of queryCommandState(), isAlreadyApplied(node),
1527         * isActive(), and setActive()
1528         */
1529        setInactive: undefined,
1530
1531        /************************ Helpers ************************
1532         * The following are helpers that are either set by MediumEditor
1533         * during initialization, or are helper methods which either
1534         * route calls to the MediumEditor instance or provide common
1535         * functionality for all extensions
1536         *********************************************************/
1537
1538        /* window: [Window]
1539         *
1540         * If not overriden, this will be set to the window object
1541         * to be used by MediumEditor and its extensions.  This is
1542         * passed via the 'contentWindow' option to MediumEditor
1543         * and is the global 'window' object by default
1544         */
1545        'window': undefined,
1546
1547        /* document: [Document]
1548         *
1549         * If not overriden, this will be set to the document object
1550         * to be used by MediumEditor and its extensions. This is
1551         * passed via the 'ownerDocument' optin to MediumEditor
1552         * and is the global 'document' object by default
1553         */
1554        'document': undefined,
1555
1556        /* getEditorElements: [function ()]
1557         *
1558         * Helper function which returns an array containing
1559         * all the contenteditable elements for this instance
1560         * of MediumEditor
1561         */
1562        getEditorElements: function () {
1563            return this.base.elements;
1564        },
1565
1566        /* getEditorId: [function ()]
1567         *
1568         * Helper function which returns a unique identifier
1569         * for this instance of MediumEditor
1570         */
1571        getEditorId: function () {
1572            return this.base.id;
1573        },
1574
1575        /* getEditorOptions: [function (option)]
1576         *
1577         * Helper function which returns the value of an option
1578         * used to initialize this instance of MediumEditor
1579         */
1580        getEditorOption: function (option) {
1581            return this.base.options[option];
1582        }
1583    };
1584
1585    /* List of method names to add to the prototype of Extension
1586     * Each of these methods will be defined as helpers that
1587     * just call directly into the MediumEditor instance.
1588     *
1589     * example for 'on' method:
1590     * Extension.prototype.on = function () {
1591     *     return this.base.on.apply(this.base, arguments);
1592     * }
1593     */
1594    [
1595        // general helpers
1596        'execAction',
1597
1598        // event handling
1599        'on',
1600        'off',
1601        'subscribe',
1602        'trigger'
1603
1604    ].forEach(function (helper) {
1605        Extension.prototype[helper] = function () {
1606            return this.base[helper].apply(this.base, arguments);
1607        };
1608    });
1609
1610    MediumEditor.Extension = Extension;
1611})();
1612
1613(function () {
1614    'use strict';
1615
1616    function filterOnlyParentElements(node) {
1617        if (MediumEditor.util.isBlockContainer(node)) {
1618            return NodeFilter.FILTER_ACCEPT;
1619        } else {
1620            return NodeFilter.FILTER_SKIP;
1621        }
1622    }
1623
1624    var Selection = {
1625        findMatchingSelectionParent: function (testElementFunction, contentWindow) {
1626            var selection = contentWindow.getSelection(),
1627                range,
1628                current;
1629
1630            if (selection.rangeCount === 0) {
1631                return false;
1632            }
1633
1634            range = selection.getRangeAt(0);
1635            current = range.commonAncestorContainer;
1636
1637            return MediumEditor.util.traverseUp(current, testElementFunction);
1638        },
1639
1640        getSelectionElement: function (contentWindow) {
1641            return this.findMatchingSelectionParent(function (el) {
1642                return MediumEditor.util.isMediumEditorElement(el);
1643            }, contentWindow);
1644        },
1645
1646        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
1647        // Tim Down
1648        exportSelection: function (root, doc) {
1649            if (!root) {
1650                return null;
1651            }
1652
1653            var selectionState = null,
1654                selection = doc.getSelection();
1655
1656            if (selection.rangeCount > 0) {
1657                var range = selection.getRangeAt(0),
1658                    preSelectionRange = range.cloneRange(),
1659                    start;
1660
1661                preSelectionRange.selectNodeContents(root);
1662                preSelectionRange.setEnd(range.startContainer, range.startOffset);
1663                start = preSelectionRange.toString().length;
1664
1665                selectionState = {
1666                    start: start,
1667                    end: start + range.toString().length
1668                };
1669                // If start = 0 there may still be an empty paragraph before it, but we don't care.
1670                if (start !== 0) {
1671                    var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset);
1672                    if (emptyBlocksIndex !== -1) {
1673                        selectionState.emptyBlocksIndex = emptyBlocksIndex;
1674                    }
1675                }
1676            }
1677
1678            return selectionState;
1679        },
1680
1681        // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
1682        // Tim Down
1683        //
1684        // {object} selectionState - the selection to import
1685        // {DOMElement} root - the root element the selection is being restored inside of
1686        // {Document} doc - the document to use for managing selection
1687        // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately
1688        //      subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the
1689        //      anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior
1690        //      in MS IE.
1691        importSelection: function (selectionState, root, doc, favorLaterSelectionAnchor) {
1692            if (!selectionState || !root) {
1693                return;
1694            }
1695
1696            var range = doc.createRange();
1697            range.setStart(root, 0);
1698            range.collapse(true);
1699
1700            var node = root,
1701                nodeStack = [],
1702                charIndex = 0,
1703                foundStart = false,
1704                stop = false,
1705                nextCharIndex;
1706
1707            while (!stop && node) {
1708                if (node.nodeType === 3) {
1709                    nextCharIndex = charIndex + node.length;
1710                    if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
1711                        range.setStart(node, selectionState.start - charIndex);
1712                        foundStart = true;
1713                    }
1714                    if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
1715                        range.setEnd(node, selectionState.end - charIndex);
1716                        stop = true;
1717                    }
1718                    charIndex = nextCharIndex;
1719                } else {
1720                    var i = node.childNodes.length - 1;
1721                    while (i >= 0) {
1722                        nodeStack.push(node.childNodes[i]);
1723                        i -= 1;
1724                    }
1725                }
1726                if (!stop) {
1727                    node = nodeStack.pop();
1728                }
1729            }
1730
1731            if (typeof selectionState.emptyBlocksIndex !== 'undefined') {
1732                range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range);
1733            }
1734
1735            // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.
1736            if (favorLaterSelectionAnchor) {
1737                range = this.importSelectionMoveCursorPastAnchor(selectionState, range);
1738            }
1739
1740            var sel = doc.getSelection();
1741            sel.removeAllRanges();
1742            sel.addRange(range);
1743        },
1744
1745        // Utility method called from importSelection only
1746        importSelectionMoveCursorPastAnchor: function (selectionState, range) {
1747            var nodeInsideAnchorTagFunction = function (node) {
1748                return node.nodeName.toLowerCase() === 'a';
1749            };
1750            if (selectionState.start === selectionState.end &&
1751                    range.startContainer.nodeType === 3 &&
1752                    range.startOffset === range.startContainer.nodeValue.length &&
1753                    MediumEditor.util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {
1754                var prevNode = range.startContainer,
1755                    currentNode = range.startContainer.parentNode;
1756                while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {
1757                    if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {
1758                        currentNode = null;
1759                    } else {
1760                        prevNode = currentNode;
1761                        currentNode = currentNode.parentNode;
1762                    }
1763                }
1764                if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {
1765                    var currentNodeIndex = null;
1766                    for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {
1767                        if (currentNode.parentNode.childNodes[i] === currentNode) {
1768                            currentNodeIndex = i;
1769                        }
1770                    }
1771                    range.setStart(currentNode.parentNode, currentNodeIndex + 1);
1772                    range.collapse(true);
1773                }
1774            }
1775            return range;
1776        },
1777
1778        // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks
1779        // to move the cursor back to the start of the correct paragraph
1780        importSelectionMoveCursorPastBlocks: function (doc, root, index, range) {
1781            var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),
1782                startContainer = range.startContainer,
1783                startBlock,
1784                targetNode,
1785                currIndex = 0;
1786            index = index || 1; // If index is 0, we still want to move to the next block
1787
1788            // Chrome counts newlines and spaces that separate block elements as actual elements.
1789            // If the selection is inside one of these text nodes, and it has a previous sibling
1790            // which is a block element, we want the treewalker to start at the previous sibling
1791            // and NOT at the parent of the textnode
1792            if (startContainer.nodeType === 3 && MediumEditor.util.isBlockContainer(startContainer.previousSibling)) {
1793                startBlock = startContainer.previousSibling;
1794            } else {
1795                startBlock = MediumEditor.util.getClosestBlockContainer(startContainer);
1796            }
1797
1798            // Skip over empty blocks until we hit the block we want the selection to be in
1799            while (treeWalker.nextNode()) {
1800                if (!targetNode) {
1801                    // Loop through all blocks until we hit the starting block element
1802                    if (startBlock === treeWalker.currentNode) {
1803                        targetNode = treeWalker.currentNode;
1804                    }
1805                } else {
1806                    targetNode = treeWalker.currentNode;
1807                    currIndex++;
1808                    // We hit the target index, bail
1809                    if (currIndex === index) {
1810                        break;
1811                    }
1812                    // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here
1813                    if (targetNode.textContent.length > 0) {
1814                        break;
1815                    }
1816                }
1817            }
1818
1819            // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest
1820            // element at the beginning of the block
1821            range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0);
1822
1823            return range;
1824        },
1825
1826        // Returns -1 unless the cursor is at the beginning of a paragraph/block
1827        // If the paragraph/block is preceeded by empty paragraphs/block (with no text)
1828        // it will return the number of empty paragraphs before the cursor.
1829        // Otherwise, it will return 0, which indicates the cursor is at the beginning
1830        // of a paragraph/block, and not at the end of the paragraph/block before it
1831        getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) {
1832            // If there is text in front of the cursor, that means there isn't only empty blocks before it
1833            if (cursorContainer.textContent.length > 0 && cursorOffset > 0) {
1834                return -1;
1835            }
1836
1837            // Check if the block that contains the cursor has any other text in front of the cursor
1838            var node = cursorContainer;
1839            if (node.nodeType !== 3) {
1840                node = cursorContainer.childNodes[cursorOffset];
1841            }
1842            if (node && !MediumEditor.util.isElementAtBeginningOfBlock(node)) {
1843                return -1;
1844            }
1845
1846            // Walk over block elements, counting number of empty blocks between last piece of text
1847            // and the block the cursor is in
1848            var closestBlock = MediumEditor.util.getClosestBlockContainer(cursorContainer),
1849                treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false),
1850                emptyBlocksCount = 0;
1851            while (treeWalker.nextNode()) {
1852                var blockIsEmpty = treeWalker.currentNode.textContent === '';
1853                if (blockIsEmpty || emptyBlocksCount > 0) {
1854                    emptyBlocksCount += 1;
1855                }
1856                if (treeWalker.currentNode === closestBlock) {
1857                    return emptyBlocksCount;
1858                }
1859                if (!blockIsEmpty) {
1860                    emptyBlocksCount = 0;
1861                }
1862            }
1863
1864            return emptyBlocksCount;
1865        },
1866
1867        selectionInContentEditableFalse: function (contentWindow) {
1868            // determine if the current selection is exclusively inside
1869            // a contenteditable="false", though treat the case of an
1870            // explicit contenteditable="true" inside a "false" as false.
1871            var sawtrue,
1872                sawfalse = this.findMatchingSelectionParent(function (el) {
1873                    var ce = el && el.getAttribute('contenteditable');
1874                    if (ce === 'true') {
1875                        sawtrue = true;
1876                    }
1877                    return el.nodeName !== '#text' && ce === 'false';
1878                }, contentWindow);
1879
1880            return !sawtrue && sawfalse;
1881        },
1882
1883        // http://stackoverflow.com/questions/4176923/html-of-selected-text
1884        // by Tim Down
1885        getSelectionHtml: function getSelectionHtml(doc) {
1886            var i,
1887                html = '',
1888                sel = doc.getSelection(),
1889                len,
1890                container;
1891            if (sel.rangeCount) {
1892                container = doc.createElement('div');
1893                for (i = 0, len = sel.rangeCount; i < len; i += 1) {
1894                    container.appendChild(sel.getRangeAt(i).cloneContents());
1895                }
1896                html = container.innerHTML;
1897            }
1898            return html;
1899        },
1900
1901        /**
1902         *  Find the caret position within an element irrespective of any inline tags it may contain.
1903         *
1904         *  @param {DOMElement} An element containing the cursor to find offsets relative to.
1905         *  @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
1906         *  @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
1907         */
1908        getCaretOffsets: function getCaretOffsets(element, range) {
1909            var preCaretRange, postCaretRange;
1910
1911            if (!range) {
1912                range = window.getSelection().getRangeAt(0);
1913            }
1914
1915            preCaretRange = range.cloneRange();
1916            postCaretRange = range.cloneRange();
1917
1918            preCaretRange.selectNodeContents(element);
1919            preCaretRange.setEnd(range.endContainer, range.endOffset);
1920
1921            postCaretRange.selectNodeContents(element);
1922            postCaretRange.setStart(range.endContainer, range.endOffset);
1923
1924            return {
1925                left: preCaretRange.toString().length,
1926                right: postCaretRange.toString().length
1927            };
1928        },
1929
1930        // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
1931        rangeSelectsSingleNode: function (range) {
1932            var startNode = range.startContainer;
1933            return startNode === range.endContainer &&
1934                startNode.hasChildNodes() &&
1935                range.endOffset === range.startOffset + 1;
1936        },
1937
1938        getSelectedParentElement: function (range) {
1939            if (!range) {
1940                return null;
1941            }
1942
1943            // Selection encompasses a single element
1944            if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
1945                return range.startContainer.childNodes[range.startOffset];
1946            }
1947
1948            // Selection range starts inside a text node, so get its parent
1949            if (range.startContainer.nodeType === 3) {
1950                return range.startContainer.parentNode;
1951            }
1952
1953            // Selection starts inside an element
1954            return range.startContainer;
1955        },
1956
1957        getSelectedElements: function (doc) {
1958            var selection = doc.getSelection(),
1959                range,
1960                toRet,
1961                currNode;
1962
1963            if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) {
1964                return [];
1965            }
1966
1967            range = selection.getRangeAt(0);
1968
1969            if (range.commonAncestorContainer.nodeType === 3) {
1970                toRet = [];
1971                currNode = range.commonAncestorContainer;
1972                while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {
1973                    toRet.push(currNode.parentNode);
1974                    currNode = currNode.parentNode;
1975                }
1976
1977                return toRet;
1978            }
1979
1980            return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {
1981                return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true;
1982            });
1983        },
1984
1985        selectNode: function (node, doc) {
1986            var range = doc.createRange(),
1987                sel = doc.getSelection();
1988
1989            range.selectNodeContents(node);
1990            sel.removeAllRanges();
1991            sel.addRange(range);
1992        },
1993
1994        select: function (doc, startNode, startOffset, endNode, endOffset) {
1995            doc.getSelection().removeAllRanges();
1996            var range = doc.createRange();
1997            range.setStart(startNode, startOffset);
1998            if (endNode) {
1999                range.setEnd(endNode, endOffset);
2000            } else {
2001                range.collapse(true);
2002            }
2003            doc.getSelection().addRange(range);
2004            return range;
2005        },
2006
2007        /**
2008         * Move cursor to the given node with the given offset.
2009         *
2010         * @param  {DomDocument} doc     Current document
2011         * @param  {DomElement}  node    Element where to jump
2012         * @param  {integer}     offset  Where in the element should we jump, 0 by default
2013         */
2014        moveCursor: function (doc, node, offset) {
2015            this.select(doc, node, offset);
2016        },
2017
2018        getSelectionRange: function (ownerDocument) {
2019            var selection = ownerDocument.getSelection();
2020            if (selection.rangeCount === 0) {
2021                return null;
2022            }
2023            return selection.getRangeAt(0);
2024        },
2025
2026        // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
2027        // by You
2028        getSelectionStart: function (ownerDocument) {
2029            var node = ownerDocument.getSelection().anchorNode,
2030                startNode = (node && node.nodeType === 3 ? node.parentNode : node);
2031
2032            return startNode;
2033        }
2034    };
2035
2036    MediumEditor.selection = Selection;
2037}());
2038
2039(function () {
2040    'use strict';
2041
2042    var Events = function (instance) {
2043        this.base = instance;
2044        this.options = this.base.options;
2045        this.events = [];
2046        this.disabledEvents = {};
2047        this.customEvents = {};
2048        this.listeners = {};
2049    };
2050
2051    Events.prototype = {
2052        InputEventOnContenteditableSupported: !MediumEditor.util.isIE,
2053
2054        // Helpers for event handling
2055
2056        attachDOMEvent: function (target, event, listener, useCapture) {
2057            target.addEventListener(event, listener, useCapture);
2058            this.events.push([target, event, listener, useCapture]);
2059        },
2060
2061        detachDOMEvent: function (target, event, listener, useCapture) {
2062            var index = this.indexOfListener(target, event, listener, useCapture),
2063                e;
2064            if (index !== -1) {
2065                e = this.events.splice(index, 1)[0];
2066                e[0].removeEventListener(e[1], e[2], e[3]);
2067            }
2068        },
2069
2070        indexOfListener: function (target, event, listener, useCapture) {
2071            var i, n, item;
2072            for (i = 0, n = this.events.length; i < n; i = i + 1) {
2073                item = this.events[i];
2074                if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
2075                    return i;
2076                }
2077            }
2078            return -1;
2079        },
2080
2081        detachAllDOMEvents: function () {
2082            var e = this.events.pop();
2083            while (e) {
2084                e[0].removeEventListener(e[1], e[2], e[3]);
2085                e = this.events.pop();
2086            }
2087        },
2088
2089        enableCustomEvent: function (event) {
2090            if (this.disabledEvents[event] !== undefined) {
2091                delete this.disabledEvents[event];
2092            }
2093        },
2094
2095        disableCustomEvent: function (event) {
2096            this.disabledEvents[event] = true;
2097        },
2098
2099        // custom events
2100        attachCustomEvent: function (event, listener) {
2101            this.setupListener(event);
2102            if (!this.customEvents[event]) {
2103                this.customEvents[event] = [];
2104            }
2105            this.customEvents[event].push(listener);
2106        },
2107
2108        detachCustomEvent: function (event, listener) {
2109            var index = this.indexOfCustomListener(event, listener);
2110            if (index !== -1) {
2111                this.customEvents[event].splice(index, 1);
2112                // TODO: If array is empty, should detach internal listeners via destroyListener()
2113            }
2114        },
2115
2116        indexOfCustomListener: function (event, listener) {
2117            if (!this.customEvents[event] || !this.customEvents[event].length) {
2118                return -1;
2119            }
2120
2121            return this.customEvents[event].indexOf(listener);
2122        },
2123
2124        detachAllCustomEvents: function () {
2125            this.customEvents = {};
2126            // TODO: Should detach internal listeners here via destroyListener()
2127        },
2128
2129        triggerCustomEvent: function (name, data, editable) {
2130            if (this.customEvents[name] && !this.disabledEvents[name]) {
2131                this.customEvents[name].forEach(function (listener) {
2132                    listener(data, editable);
2133                });
2134            }
2135        },
2136
2137        // Cleaning up
2138
2139        destroy: function () {
2140            this.detachAllDOMEvents();
2141            this.detachAllCustomEvents();
2142            this.detachExecCommand();
2143
2144            if (this.base.elements) {
2145                this.base.elements.forEach(function (element) {
2146                    element.removeAttribute('data-medium-focused');
2147                });
2148            }
2149        },
2150
2151        // Listening to calls to document.execCommand
2152
2153        // Attach a listener to be notified when document.execCommand is called
2154        attachToExecCommand: function () {
2155            if (this.execCommandListener) {
2156                return;
2157            }
2158
2159            // Store an instance of the listener so:
2160            // 1) We only attach to execCommand once
2161            // 2) We can remove the listener later
2162            this.execCommandListener = function (execInfo) {
2163                this.handleDocumentExecCommand(execInfo);
2164            }.bind(this);
2165
2166            // Ensure that execCommand has been wrapped correctly
2167            this.wrapExecCommand();
2168
2169            // Add listener to list of execCommand listeners
2170            this.options.ownerDocument.execCommand.listeners.push(this.execCommandListener);
2171        },
2172
2173        // Remove our listener for calls to document.execCommand
2174        detachExecCommand: function () {
2175            var doc = this.options.ownerDocument;
2176            if (!this.execCommandListener || !doc.execCommand.listeners) {
2177                return;
2178            }
2179
2180            // Find the index of this listener in the array of listeners so it can be removed
2181            var index = doc.execCommand.listeners.indexOf(this.execCommandListener);
2182            if (index !== -1) {
2183                doc.execCommand.listeners.splice(index, 1);
2184            }
2185
2186            // If the list of listeners is now empty, put execCommand back to its original state
2187            if (!doc.execCommand.listeners.length) {
2188                this.unwrapExecCommand();
2189            }
2190        },
2191
2192        // Wrap document.execCommand in a custom method so we can listen to calls to it
2193        wrapExecCommand: function () {
2194            var doc = this.options.ownerDocument;
2195
2196            // Ensure all instance of MediumEditor only wrap execCommand once
2197            if (doc.execCommand.listeners) {
2198                return;
2199            }
2200
2201            // Create a wrapper method for execCommand which will:
2202            // 1) Call document.execCommand with the correct arguments
2203            // 2) Loop through any listeners and notify them that execCommand was called
2204            //    passing extra info on the call
2205            // 3) Return the result
2206            var wrapper = function (aCommandName, aShowDefaultUI, aValueArgument) {
2207                var result = doc.execCommand.orig.apply(this, arguments);
2208
2209                if (!doc.execCommand.listeners) {
2210                    return result;
2211                }
2212
2213                var args = Array.prototype.slice.call(arguments);
2214                doc.execCommand.listeners.forEach(function (listener) {
2215                    listener({
2216                        command: aCommandName,
2217                        value: aValueArgument,
2218                        args: args,
2219                        result: result
2220                    });
2221                });
2222
2223                return result;
2224            };
2225
2226            // Store a reference to the original execCommand
2227            wrapper.orig = doc.execCommand;
2228
2229            // Attach an array for storing listeners
2230            wrapper.listeners = [];
2231
2232            // Overwrite execCommand
2233            doc.execCommand = wrapper;
2234        },
2235
2236        // Revert document.execCommand back to its original self
2237        unwrapExecCommand: function () {
2238            var doc = this.options.ownerDocument;
2239            if (!doc.execCommand.orig) {
2240                return;
2241            }
2242
2243            // Use the reference to the original execCommand to revert back
2244            doc.execCommand = doc.execCommand.orig;
2245        },
2246
2247        // Listening to browser events to emit events medium-editor cares about
2248        setupListener: function (name) {
2249            if (this.listeners[name]) {
2250                return;
2251            }
2252
2253            switch (name) {
2254                case 'externalInteraction':
2255                    // Detecting when user has interacted with elements outside of MediumEditor
2256                    this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true);
2257                    this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true);
2258                    this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true);
2259                    break;
2260                case 'blur':
2261                    // Detecting when focus is lost
2262                    this.setupListener('externalInteraction');
2263                    break;
2264                case 'focus':
2265                    // Detecting when focus moves into some part of MediumEditor
2266                    this.setupListener('externalInteraction');
2267                    break;
2268                case 'editableInput':
2269                    // setup cache for knowing when the content has changed
2270                    this.contentCache = [];
2271                    this.base.elements.forEach(function (element) {
2272                        this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML;
2273
2274                        // Attach to the 'oninput' event, handled correctly by most browsers
2275                        if (this.InputEventOnContenteditableSupported) {
2276                            this.attachDOMEvent(element, 'input', this.handleInput.bind(this));
2277                        }
2278                    }.bind(this));
2279
2280                    // For browsers which don't support the input event on contenteditable (IE)
2281                    // we'll attach to 'selectionchange' on the document and 'keypress' on the editables
2282                    if (!this.InputEventOnContenteditableSupported) {
2283                        this.setupListener('editableKeypress');
2284                        this.keypressUpdateInput = true;
2285                        this.attachDOMEvent(document, 'selectionchange', this.handleDocumentSelectionChange.bind(this));
2286                        // Listen to calls to execCommand
2287                        this.attachToExecCommand();
2288                    }
2289                    break;
2290                case 'editableClick':
2291                    // Detecting click in the contenteditables
2292                    this.attachToEachElement('click', this.handleClick);
2293                    break;
2294                case 'editableBlur':
2295                    // Detecting blur in the contenteditables
2296                    this.attachToEachElement('blur', this.handleBlur);
2297                    break;
2298                case 'editableKeypress':
2299                    // Detecting keypress in the contenteditables
2300                    this.attachToEachElement('keypress', this.handleKeypress);
2301                    break;
2302                case 'editableKeyup':
2303                    // Detecting keyup in the contenteditables
2304                    this.attachToEachElement('keyup', this.handleKeyup);
2305                    break;
2306                case 'editableKeydown':
2307                    // Detecting keydown on the contenteditables
2308                    this.attachToEachElement('keydown', this.handleKeydown);
2309                    break;
2310                case 'editableKeydownEnter':
2311                    // Detecting keydown for ENTER on the contenteditables
2312                    this.setupListener('editableKeydown');
2313                    break;
2314                case 'editableKeydownTab':
2315                    // Detecting keydown for TAB on the contenteditable
2316                    this.setupListener('editableKeydown');
2317                    break;
2318                case 'editableKeydownDelete':
2319                    // Detecting keydown for DELETE/BACKSPACE on the contenteditables
2320                    this.setupListener('editableKeydown');
2321                    break;
2322                case 'editableMouseover':
2323                    // Detecting mouseover on the contenteditables
2324                    this.attachToEachElement('mouseover', this.handleMouseover);
2325                    break;
2326                case 'editableDrag':
2327                    // Detecting dragover and dragleave on the contenteditables
2328                    this.attachToEachElement('dragover', this.handleDragging);
2329                    this.attachToEachElement('dragleave', this.handleDragging);
2330                    break;
2331                case 'editableDrop':
2332                    // Detecting drop on the contenteditables
2333                    this.attachToEachElement('drop', this.handleDrop);
2334                    break;
2335                case 'editablePaste':
2336                    // Detecting paste on the contenteditables
2337                    this.attachToEachElement('paste', this.handlePaste);
2338                    break;
2339            }
2340            this.listeners[name] = true;
2341        },
2342
2343        attachToEachElement: function (name, handler) {
2344            this.base.elements.forEach(function (element) {
2345                this.attachDOMEvent(element, name, handler.bind(this));
2346            }, this);
2347        },
2348
2349        focusElement: function (element) {
2350            element.focus();
2351            this.updateFocus(element, { target: element, type: 'focus' });
2352        },
2353
2354        updateFocus: function (target, eventObj) {
2355            var toolbar = this.base.getExtensionByName('toolbar'),
2356                toolbarEl = toolbar ? toolbar.getToolbarElement() : null,
2357                anchorPreview = this.base.getExtensionByName('anchor-preview'),
2358                previewEl = (anchorPreview && anchorPreview.getPreviewElement) ? anchorPreview.getPreviewElement() : null,
2359                hadFocus = this.base.getFocusedElement(),
2360                toFocus;
2361
2362            // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element.
2363            // If so, we don't want to focus another element
2364            if (hadFocus &&
2365                    eventObj.type === 'click' &&
2366                    this.lastMousedownTarget &&
2367                    (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) ||
2368                     MediumEditor.util.isDescendant(toolbarEl, this.lastMousedownTarget, true) ||
2369                     MediumEditor.util.isDescendant(previewEl, this.lastMousedownTarget, true))) {
2370                toFocus = hadFocus;
2371            }
2372
2373            if (!toFocus) {
2374                this.base.elements.some(function (element) {
2375                    // If the target is part of an editor element, this is the element getting focus
2376                    if (!toFocus && (MediumEditor.util.isDescendant(element, target, true))) {
2377                        toFocus = element;
2378                    }
2379
2380                    // bail if we found an element that's getting focus
2381                    return !!toFocus;
2382                }, this);
2383            }
2384
2385            // Check if the target is external (not part of the editor, toolbar, or anchorpreview)
2386            var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) &&
2387                                !MediumEditor.util.isDescendant(toolbarEl, target, true) &&
2388                                !MediumEditor.util.isDescendant(previewEl, target, true);
2389
2390            if (toFocus !== hadFocus) {
2391                // If element has focus, and focus is going outside of editor
2392                // Don't blur focused element if clicking on editor, toolbar, or anchorpreview
2393                if (hadFocus && externalEvent) {
2394                    // Trigger blur on the editable that has lost focus
2395                    hadFocus.removeAttribute('data-medium-focused');
2396                    this.triggerCustomEvent('blur', eventObj, hadFocus);
2397                }
2398
2399                // If focus is going into an editor element
2400                if (toFocus) {
2401                    // Trigger focus on the editable that now has focus
2402                    toFocus.setAttribute('data-medium-focused', true);
2403                    this.triggerCustomEvent('focus', eventObj, toFocus);
2404                }
2405            }
2406
2407            if (externalEvent) {
2408                this.triggerCustomEvent('externalInteraction', eventObj);
2409            }
2410        },
2411
2412        updateInput: function (target, eventObj) {
2413            if (!this.contentCache) {
2414                return;
2415            }
2416            // An event triggered which signifies that the user may have changed someting
2417            // Look in our cache of input for the contenteditables to see if something changed
2418            var index = target.getAttribute('medium-editor-index');
2419            if (target.innerHTML !== this.contentCache[index]) {
2420                // The content has changed since the last time we checked, fire the event
2421                this.triggerCustomEvent('editableInput', eventObj, target);
2422            }
2423            this.contentCache[index] = target.innerHTML;
2424        },
2425
2426        handleDocumentSelectionChange: function (event) {
2427            // When selectionchange fires, target and current target are set
2428            // to document, since this is where the event is handled
2429            // However, currentTarget will have an 'activeElement' property
2430            // which will point to whatever element has focus.
2431            if (event.currentTarget && event.currentTarget.activeElement) {
2432                var activeElement = event.currentTarget.activeElement,
2433                    currentTarget;
2434                // We can look at the 'activeElement' to determine if the selectionchange has
2435                // happened within a contenteditable owned by this instance of MediumEditor
2436                this.base.elements.some(function (element) {
2437                    if (MediumEditor.util.isDescendant(element, activeElement, true)) {
2438                        currentTarget = element;
2439                        return true;
2440                    }
2441                    return false;
vendor: 2,978 bytes, lines 2442-2522
2442                }, this);
2443
2444                // We know selectionchange fired within one of our contenteditables
2445                if (currentTarget) {
2446                    this.updateInput(currentTarget, { target: activeElement, currentTarget: currentTarget });
2447                }
2448            }
2449        },
2450
2451        handleDocumentExecCommand: function () {
2452            // document.execCommand has been called
2453            // If one of our contenteditables currently has focus, we should
2454            // attempt to trigger the 'editableInput' event
2455            var target = this.base.getFocusedElement();
2456            if (target) {
2457                this.updateInput(target, { target: target, currentTarget: target });
2458            }
2459        },
2460
2461        handleBodyClick: function (event) {
2462            this.updateFocus(event.target, event);
2463        },
2464
2465        handleBodyFocus: function (event) {
2466            this.updateFocus(event.target, event);
2467        },
2468
2469        handleBodyMousedown: function (event) {
2470            this.lastMousedownTarget = event.target;
2471        },
2472
2473        handleInput: function (event) {
2474            this.updateInput(event.currentTarget, event);
2475        },
2476
2477        handleClick: function (event) {
2478            this.triggerCustomEvent('editableClick', event, event.currentTarget);
2479        },
2480
2481        handleBlur: function (event) {
2482            this.triggerCustomEvent('editableBlur', event, event.currentTarget);
2483        },
2484
2485        handleKeypress: function (event) {
2486            this.triggerCustomEvent('editableKeypress', event, event.currentTarget);
2487
2488            // If we're doing manual detection of the editableInput event we need
2489            // to check for input changes during 'keypress'
2490            if (this.keypressUpdateInput) {
2491                var eventObj = { target: event.target, currentTarget: event.currentTarget };
2492
2493                // In IE, we need to let the rest of the event stack complete before we detect
2494                // changes to input, so using setTimeout here
2495                setTimeout(function () {
2496                    this.updateInput(eventObj.currentTarget, eventObj);
2497                }.bind(this), 0);
2498            }
2499        },
2500
2501        handleKeyup: function (event) {
2502            this.triggerCustomEvent('editableKeyup', event, event.currentTarget);
2503        },
2504
2505        handleMouseover: function (event) {
2506            this.triggerCustomEvent('editableMouseover', event, event.currentTarget);
2507        },
2508
2509        handleDragging: function (event) {
2510            this.triggerCustomEvent('editableDrag', event, event.currentTarget);
2511        },
2512
2513        handleDrop: function (event) {
2514            this.triggerCustomEvent('editableDrop', event, event.currentTarget);
2515        },
2516
2517        handlePaste: function (event) {
2518            this.triggerCustomEvent('editablePaste', event, event.currentTarget);
2519        },
2520
2521        handleKeydown: function (event) {
2522            this.triggerCustomEvent('editableKeydown', event, event.currentTarget);
vendor: 12,596 bytes, lines 2522-2866
2522
2523
2524            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) || (event.ctrlKey && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.M))) {
2525                return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget);
2526            }
2527
2528            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.TAB)) {
2529                return this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget);
2530            }
2531
2532            if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.DELETE, MediumEditor.util.keyCode.BACKSPACE])) {
2533                return this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget);
2534            }
2535        }
2536    };
2537
2538    MediumEditor.Events = Events;
2539}());
2540
2541(function () {
2542    'use strict';
2543
2544    var Button = MediumEditor.Extension.extend({
2545
2546        /* Button Options */
2547
2548        /* action: [string]
2549         * The action argument to pass to MediumEditor.execAction()
2550         * when the button is clicked
2551         */
2552        action: undefined,
2553
2554        /* aria: [string]
2555         * The value to add as the aria-label attribute of the button
2556         * element displayed in the toolbar.
2557         * This is also used as the tooltip for the button
2558         */
2559        aria: undefined,
2560
2561        /* tagNames: [Array]
2562         * NOTE: This is not used if useQueryState is set to true.
2563         *
2564         * Array of element tag names that would indicate that this
2565         * button has already been applied. If this action has already
2566         * been applied, the button will be displayed as 'active' in the toolbar
2567         *
2568         * Example:
2569         * For 'bold', if the text is ever within a <b> or <strong>
2570         * tag that indicates the text is already bold. So the array
2571         * of tagNames for bold would be: ['b', 'strong']
2572         */
2573        tagNames: undefined,
2574
2575        /* style: [Object]
2576         * NOTE: This is not used if useQueryState is set to true.
2577         *
2578         * A pair of css property & value(s) that indicate that this
2579         * button has already been applied. If this action has already
2580         * been applied, the button will be displayed as 'active' in the toolbar
2581         * Properties of the object:
2582         *   prop [String]: name of the css property
2583         *   value [String]: value(s) of the css property
2584         *                   multiple values can be separated by a '|'
2585         *
2586         * Example:
2587         * For 'bold', if the text is ever within an element with a 'font-weight'
2588         * style property set to '700' or 'bold', that indicates the text
2589         * is already bold.  So the style object for bold would be:
2590         * { prop: 'font-weight', value: '700|bold' }
2591         */
2592        style: undefined,
2593
2594        /* useQueryState: [boolean]
2595         * Enables/disables whether this button should use the built-in
2596         * document.queryCommandState() method to determine whether
2597         * the action has already been applied.  If the action has already
2598         * been applied, the button will be displayed as 'active' in the toolbar
2599         *
2600         * Example:
2601         * For 'bold', if this is set to true, the code will call:
2602         * document.queryCommandState('bold') which will return true if the
2603         * browser thinks the text is already bold, and false otherwise
2604         */
2605        useQueryState: undefined,
2606
2607        /* contentDefault: [string]
2608         * Default innerHTML to put inside the button
2609         */
2610        contentDefault: undefined,
2611
2612        /* contentFA: [string]
2613         * The innerHTML to use for the content of the button
2614         * if the `buttonLabels` option for MediumEditor is set to 'fontawesome'
2615         */
2616        contentFA: undefined,
2617
2618        /* classList: [Array]
2619         * An array of classNames (strings) to be added to the button
2620         */
2621        classList: undefined,
2622
2623        /* attrs: [object]
2624         * A set of key-value pairs to add to the button as custom attributes
2625         */
2626        attrs: undefined,
2627
2628        // The button constructor can optionally accept the name of a built-in button
2629        // (ie 'bold', 'italic', etc.)
2630        // When the name of a button is passed, it will initialize itself with the
2631        // configuration for that button
2632        constructor: function (options) {
2633            if (Button.isBuiltInButton(options)) {
2634                MediumEditor.Extension.call(this, this.defaults[options]);
2635            } else {
2636                MediumEditor.Extension.call(this, options);
2637            }
2638        },
2639
2640        init: function () {
2641            MediumEditor.Extension.prototype.init.apply(this, arguments);
2642
2643            this.button = this.createButton();
2644            this.on(this.button, 'click', this.handleClick.bind(this));
2645        },
2646
2647        /* getButton: [function ()]
2648         *
2649         * If implemented, this function will be called when
2650         * the toolbar is being created.  The DOM Element returned
2651         * by this function will be appended to the toolbar along
2652         * with any other buttons.
2653         */
2654        getButton: function () {
2655            return this.button;
2656        },
2657
2658        getAction: function () {
2659            return (typeof this.action === 'function') ? this.action(this.base.options) : this.action;
2660        },
2661
2662        getAria: function () {
2663            return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria;
2664        },
2665
2666        getTagNames: function () {
2667            return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames;
2668        },
2669
2670        createButton: function () {
2671            var button = this.document.createElement('button'),
2672                content = this.contentDefault,
2673                ariaLabel = this.getAria(),
2674                buttonLabels = this.getEditorOption('buttonLabels');
2675            // Add class names
2676            button.classList.add('medium-editor-action');
2677            button.classList.add('medium-editor-action-' + this.name);
2678            if (this.classList) {
2679                this.classList.forEach(function (className) {
2680                    button.classList.add(className);
2681                });
2682            }
2683
2684            // Add attributes
2685            button.setAttribute('data-action', this.getAction());
2686            if (ariaLabel) {
2687                button.setAttribute('title', ariaLabel);
2688                button.setAttribute('aria-label', ariaLabel);
2689            }
2690            if (this.attrs) {
2691                Object.keys(this.attrs).forEach(function (attr) {
2692                    button.setAttribute(attr, this.attrs[attr]);
2693                }, this);
2694            }
2695
2696            if (buttonLabels === 'fontawesome' && this.contentFA) {
2697                content = this.contentFA;
2698            }
2699            button.innerHTML = content;
2700            return button;
2701        },
2702
2703        handleClick: function (event) {
2704            event.preventDefault();
2705            event.stopPropagation();
2706
2707            var action = this.getAction();
2708
2709            if (action) {
2710                this.execAction(action);
2711            }
2712        },
2713
2714        isActive: function () {
2715            return this.button.classList.contains(this.getEditorOption('activeButtonClass'));
2716        },
2717
2718        setInactive: function () {
2719            this.button.classList.remove(this.getEditorOption('activeButtonClass'));
2720            delete this.knownState;
2721        },
2722
2723        setActive: function () {
2724            this.button.classList.add(this.getEditorOption('activeButtonClass'));
2725            delete this.knownState;
2726        },
2727
2728        queryCommandState: function () {
2729            var queryState = null;
2730            if (this.useQueryState) {
2731                queryState = this.base.queryCommandState(this.getAction());
2732            }
2733            return queryState;
2734        },
2735
2736        isAlreadyApplied: function (node) {
2737            var isMatch = false,
2738                tagNames = this.getTagNames(),
2739                styleVals,
2740                computedStyle;
2741
2742            if (this.knownState === false || this.knownState === true) {
2743                return this.knownState;
2744            }
2745
2746            if (tagNames && tagNames.length > 0) {
2747                isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1;
2748            }
2749
2750            if (!isMatch && this.style) {
2751                styleVals = this.style.value.split('|');
2752                computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop);
2753                styleVals.forEach(function (val) {
2754                    if (!this.knownState) {
2755                        isMatch = (computedStyle.indexOf(val) !== -1);
2756                        // text-decoration is not inherited by default
2757                        // so if the computed style for text-decoration doesn't match
2758                        // don't write to knownState so we can fallback to other checks
2759                        if (isMatch || this.style.prop !== 'text-decoration') {
2760                            this.knownState = isMatch;
2761                        }
2762                    }
2763                }, this);
2764            }
2765
2766            return isMatch;
2767        }
2768    });
2769
2770    Button.isBuiltInButton = function (name) {
2771        return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name);
2772    };
2773
2774    MediumEditor.extensions.button = Button;
2775}());
2776
2777(function () {
2778    'use strict';
2779
2780    /* MediumEditor.extensions.button.defaults: [Object]
2781     * Set of default config options for all of the built-in MediumEditor buttons
2782     */
2783    MediumEditor.extensions.button.prototype.defaults = {
2784        'bold': {
2785            name: 'bold',
2786            action: 'bold',
2787            aria: 'bold',
2788            tagNames: ['b', 'strong'],
2789            style: {
2790                prop: 'font-weight',
2791                value: '700|bold'
2792            },
2793            useQueryState: true,
2794            contentDefault: '<b>B</b>',
2795            contentFA: '<i class="fa fa-bold"></i>'
2796        },
2797        'italic': {
2798            name: 'italic',
2799            action: 'italic',
2800            aria: 'italic',
2801            tagNames: ['i', 'em'],
2802            style: {
2803                prop: 'font-style',
2804                value: 'italic'
2805            },
2806            useQueryState: true,
2807            contentDefault: '<b><i>I</i></b>',
2808            contentFA: '<i class="fa fa-italic"></i>'
2809        },
2810        'underline': {
2811            name: 'underline',
2812            action: 'underline',
2813            aria: 'underline',
2814            tagNames: ['u'],
2815            style: {
2816                prop: 'text-decoration',
2817                value: 'underline'
2818            },
2819            useQueryState: true,
2820            contentDefault: '<b><u>U</u></b>',
2821            contentFA: '<i class="fa fa-underline"></i>'
2822        },
2823        'strikethrough': {
2824            name: 'strikethrough',
2825            action: 'strikethrough',
2826            aria: 'strike through',
2827            tagNames: ['strike'],
2828            style: {
2829                prop: 'text-decoration',
2830                value: 'line-through'
2831            },
2832            useQueryState: true,
2833            contentDefault: '<s>A</s>',
2834            contentFA: '<i class="fa fa-strikethrough"></i>'
2835        },
2836        'superscript': {
2837            name: 'superscript',
2838            action: 'superscript',
2839            aria: 'superscript',
2840            tagNames: ['sup'],
2841            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript
2842               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
2843            // useQueryState: true
2844            contentDefault: '<b>x<sup>1</sup></b>',
2845            contentFA: '<i class="fa fa-superscript"></i>'
2846        },
2847        'subscript': {
2848            name: 'subscript',
2849            action: 'subscript',
2850            aria: 'subscript',
2851            tagNames: ['sub'],
2852            /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript
2853               https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */
2854            // useQueryState: true
2855            contentDefault: '<b>x<sub>1</sub></b>',
2856            contentFA: '<i class="fa fa-subscript"></i>'
2857        },
2858        'image': {
2859            name: 'image',
2860            action: 'image',
2861            aria: 'image',
2862            tagNames: ['img'],
2863            contentDefault: '<b>image</b>',
2864            contentFA: '<i class="fa fa-picture-o"></i>'
2865        },
2866        '
2866orderedlist': {
2867            name: 'orderedlist',
2868            action: 'insertorderedlist',
2869            aria: 'ordered list',
2870            tagNames: ['ol'],
2871            useQueryState: true,
2872            contentDefault: '<b>1.</b>',
2873            contentFA: '<i class="fa fa-list-ol"></i>'
2874        },
2875        'unorderedlist': {
2876            name: 'unorderedlist',
2877            action: 'insertunorderedlist',
2878            aria: 'unordered list',
2879            tagNames: ['ul'],
2880            useQueryState: true,
2881            contentDefault: '<b>&bull;</b>',
2882            contentFA: '<i class="fa fa-list-ul"></i>'
2883        },
2884        'indent': {
2885            name: 'indent',
2886            action: 'indent',
2887            aria: 'indent',
2888            tagNames: [],
2889            contentDefault: '<b>&rarr;</b>',
2890            contentFA: '<i class="fa fa-indent"></i>'
2891        },
2892        'outdent': {
2893            name: 'outdent',
2894            action: 'outdent',
2895            aria: 'outdent',
2896            tagNames: [],
2897            contentDefault: '<b>&larr;</b>',
2898            contentFA: '<i class="fa fa-outdent"></i>'
2899        },
2900        'justifyCenter': {
2901            name: 'justifyCenter',
2902            action: 'justifyCenter',
2903            aria: 'center justify',
2904            tagNames: [],
2905            style: {
2906                prop: 'text-align',
2907                value: 'center'
2908            },
2909            contentDefault: '<b>C</b>',
2910            contentFA: '<i class="fa fa-align-center"></i>'
2911        },
2912        'justifyFull': {
2913            name: 'justifyFull',
2914            action: 'justifyFull',
2915            aria: 'full justify',
2916            tagNames: [],
2917            style: {
2918                prop: 'text-align',
2919                value: 'justify'
2920            },
2921            contentDefault: '<b>J</b>',
2922            contentFA: '<i class="fa fa-align-justify"></i>'
2923        },
2924        'justifyLeft': {
2925            name: 'justifyLeft',
2926            action: 'justifyLeft',
2927            aria: 'left justify',
2928            tagNames: [],
2929            style: {
2930                prop: 'text-align',
2931                value: 'left'
2932            },
2933            contentDefault: '<b>L</b>',
2934            contentFA: '<i class="fa fa-align-left"></i>'
2935        },
2936        'justifyRight': {
2937            name: 'justifyRight',
2938            action: 'justifyRight',
2939            aria: 'right justify',
2940            tagNames: [],
2941            style: {
2942                prop: 'text-align',
2943                value: 'right'
2944            },
2945            contentDefault: '<b>R</b>',
2946            contentFA: '<i class="fa fa-align-right"></i>'
2947        },
2948        // Known inline elements that are not removed, or not removed consistantly across browsers:
2949        // <span>, <label>, <br>
2950        'removeFormat': {
2951            name: 'removeFormat',
2952            aria: 'remove formatting',
2953            action: 'removeFormat',
2954            contentDefault: '<b>X</b>',
2955            contentFA: '<i class="fa fa-eraser"></i>'
2956        },
2957
2958        /***** Buttons for appending block elements (append-<element> action) *****/
2959
2960        'quote': {
2961            name: 'quote',
2962            action: 'append-blockquote',
2963            aria: 'blockquote',
2964            tagNames: ['blockquote'],
2965            contentDefault: '<b>&ldquo;</b>',
2966            contentFA: '<i class="fa fa-quote-right"></i>'
2967        },
2968        'pre': {
2969            name: 'pre',
2970            action: 'append-pre',
2971            aria: 'preformatted text',
2972            tagNames: ['pre'],
2973            contentDefault: '<b>0101</b>',
2974            contentFA: '<i class="fa fa-code fa-lg"></i>'
2975        },
2976        'h1': {
2977            name: 'h1',
2978            action: 'append-h1',
2979            aria: 'header type one',
2980            tagNames: ['h1'],
2981            contentDefault: '<b>H1</b>',
2982            contentFA: '<i class="fa fa-header"><sup>1</sup>'
2983        },
2984        'h2': {
2985            name: 'h2',
2986            action: 'append-h2',
2987            aria: 'header type two',
2988            tagNames: ['h2'],
2989            contentDefault: '<b>H2</b>',
2990            contentFA: '<i class="fa fa-header"><sup>2</sup>'
2991        },
2992        'h3': {
2993            name: 'h3',
2994            action: 'append-h3',
2995            aria: 'header type three',
2996            tagNames: ['h3'],
2997            contentDefault: '<b>H3</b>',
2998            contentFA: '<i class="fa fa-header"><sup>3</sup>'
2999        },
3000        'h4': {
3001            name: 'h4',
3002            action: 'append-h4',
3003            aria: 'header type four',
3004            tagNames: ['h4'],
3005            contentDefault: '<b>H4</b>',
3006            contentFA: '<i class="fa fa-header"><sup>4</sup>'
3007        },
3008        'h5': {
3009            name: 'h5',
3010            action: 'append-h5',
3011            aria: 'header type five',
3012            tagNames: ['h5'],
3013            contentDefault: '<b>H5</b>',
3014            contentFA: '<i class="fa fa-header"><sup>5</sup>'
3015        },
3016        'h6': {
3017            name: 'h6',
3018            action: 'append-h6',
3019            aria: 'header type six',
3020            tagNames: ['h6'],
3021            contentDefault: '<b>H6</b>',
3022            contentFA: '<i class="fa fa-header"><sup>6</sup>'
3023        }
3024    };
3025
3026})();
3027(function () {
3028    'use strict';
3029
3030    /* Base functionality for an extension which will display
3031     * a 'form' inside the toolbar
3032     */
3033    var FormExtension = MediumEditor.extensions.button.extend({
3034
3035        init: function () {
3036            MediumEditor.extensions.button.prototype.init.apply(this, arguments);
3037        },
3038
3039        // default labels for the form buttons
3040        formSaveLabel: '&#10003;',
3041        formCloseLabel: '&times;',
3042
3043        /* hasForm: [boolean]
3044         *
3045         * Setting this to true will cause getForm() to be called
3046         * when the toolbar is created, so the form can be appended
3047         * inside the toolbar container
3048         */
3049        hasForm: true,
3050
3051        /* getForm: [function ()]
3052         *
3053         * When hasForm is true, this function must be implemented
3054         * and return a DOM Element which will be appended to
3055         * the toolbar container. The form should start hidden, and
3056         * the extension can choose when to hide/show it
3057         */
3058        getForm: function () {},
3059
3060        /* isDisplayed: [function ()]
3061         *
3062         * This function should return true/false reflecting
3063         * whether the form is currently displayed
3064         */
3065        isDisplayed: function () {},
3066
3067        /* hideForm: [function ()]
3068         *
3069         * This function should hide the form element inside
3070         * the toolbar container
3071         */
3072        hideForm: function () {},
3073
3074        /************************ Helpers ************************
3075         * The following are helpers that are either set by MediumEditor
3076         * during initialization, or are helper methods which either
3077         * route calls to the MediumEditor instance or provide common
3078         * functionality for all form extensions
3079         *********************************************************/
3080
3081        /* showToolbarDefaultActions: [function ()]
3082         *
3083         * Helper method which will turn back the toolbar after canceling
3084         * the customized form
3085         */
3086        showToolbarDefaultActions: function () {
3087            var toolbar = this.base.getExtensionByName('toolbar');
3088            if (toolbar) {
3089                toolbar.showToolbarDefaultActions();
3090            }
3091        },
3092
3093        /* hideToolbarDefaultActions: [function ()]
3094         *
3095         * Helper function which will hide the default contents of the
3096         * toolbar, but leave the toolbar container in the same state
3097         * to allow a form to display its custom contents inside the toolbar
3098         */
3099        hideToolbarDefaultActions: function () {
3100            var toolbar = this.base.getExtensionByName('toolbar');
3101            if (toolbar) {
3102                toolbar.hideToolbarDefaultActions();
3103            }
3104        },
3105
3106        /* setToolbarPosition: [function ()]
3107         *
3108         * Helper function which will update the size and position
3109         * of the toolbar based on the toolbar content and the current
3110         * position of the user's selection
3111         */
3112        setToolbarPosition: function () {
3113            var toolbar = this.base.getExtensionByName('toolbar');
3114            if (toolbar) {
3115                toolbar.setToolbarPosition();
3116            }
3117        }
3118    });
3119
3120    MediumEditor.extensions.form = FormExtension;
3121})();
3122(function () {
3123    'use strict';
3124
3125    var AnchorForm = MediumEditor.extensions.form.extend({
3126        /* Anchor Form Options */
3127
3128        /* customClassOption: [string]  (previously options.anchorButton + options.anchorButtonClass)
3129         * Custom class name the user can optionally have added to their created links (ie 'button').
3130         * If passed as a non-empty string, a checkbox will be displayed allowing the user to choose
3131         * whether to have the class added to the created link or not.
3132         */
3133        customClassOption: null,
3134
3135        /* customClassOptionText: [string]
3136         * text to be shown in the checkbox when the __customClassOption__ is being used.
3137         */
3138        customClassOptionText: 'Button',
3139
3140        /* linkValidation: [boolean]  (previously options.checkLinkFormat)
vendor: 11,000 bytes, lines 3141-3453
3141         * enables/disables check for common URL protocols on anchor links.
3142         */
3143        linkValidation: false,
3144
3145        /* placeholderText: [string]  (previously options.anchorInputPlaceholder)
3146         * text to be shown as placeholder of the anchor input.
3147         */
3148        placeholderText: 'Paste or type a link',
3149
3150        /* targetCheckbox: [boolean]  (previously options.anchorTarget)
3151         * enables/disables displaying a "Open in new window" checkbox, which when checked
3152         * changes the `target` attribute of the created link.
3153         */
3154        targetCheckbox: false,
3155
3156        /* targetCheckboxText: [string]  (previously options.anchorInputCheckboxLabel)
3157         * text to be shown in the checkbox enabled via the __targetCheckbox__ option.
3158         */
3159        targetCheckboxText: 'Open in new window',
3160
3161        // Options for the Button base class
3162        name: 'anchor',
3163        action: 'createLink',
3164        aria: 'link',
3165        tagNames: ['a'],
3166        contentDefault: '<b>#</b>',
3167        contentFA: '<i class="fa fa-link"></i>',
3168
3169        init: function () {
3170            MediumEditor.extensions.form.prototype.init.apply(this, arguments);
3171
3172            this.subscribe('editableKeydown', this.handleKeydown.bind(this));
3173        },
3174
3175        // Called when the button the toolbar is clicked
3176        // Overrides ButtonExtension.handleClick
3177        handleClick: function (event) {
3178            event.preventDefault();
3179            event.stopPropagation();
3180
3181            var range = MediumEditor.selection.getSelectionRange(this.document);
3182
3183            if (range.startContainer.nodeName.toLowerCase() === 'a' ||
3184                range.endContainer.nodeName.toLowerCase() === 'a' ||
3185                MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'a')) {
3186                return this.execAction('unlink');
3187            }
3188
3189            if (!this.isDisplayed()) {
3190                this.showForm();
3191            }
3192
3193            return false;
3194        },
3195
3196        // Called when user hits the defined shortcut (CTRL / COMMAND + K)
3197        handleKeydown: function (event) {
3198            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.K) && MediumEditor.util.isMetaCtrlKey(event) && !event.shiftKey) {
3199                this.handleClick(event);
3200            }
3201        },
3202
3203        // Called by medium-editor to append form to the toolbar
3204        getForm: function () {
3205            if (!this.form) {
3206                this.form = this.createForm();
3207            }
3208            return this.form;
3209        },
3210
3211        getTemplate: function () {
3212            var template = [
3213                '<input type="text" class="medium-editor-toolbar-input" placeholder="', this.placeholderText, '">'
3214            ];
3215
3216            template.push(
3217                '<a href="#" class="medium-editor-toolbar-save">',
3218                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-check"></i>' : this.formSaveLabel,
3219                '</a>'
3220            );
3221
3222            template.push('<a href="#" class="medium-editor-toolbar-close">',
3223                this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-times"></i>' : this.formCloseLabel,
3224                '</a>');
3225
3226            // both of these options are slightly moot with the ability to
3227            // override the various form buildup/serialize functions.
3228
3229            if (this.targetCheckbox) {
3230                // fixme: ideally, this targetCheckboxText would be a formLabel too,
3231                // figure out how to deprecate? also consider `fa-` icon default implcations.
3232                template.push(
3233                    '<div class="medium-editor-toolbar-form-row">',
3234                    '<input type="checkbox" class="medium-editor-toolbar-anchor-target">',
3235                    '<label>',
3236                    this.targetCheckboxText,
3237                    '</label>',
3238                    '</div>'
3239                );
3240            }
3241
3242            if (this.customClassOption) {
3243                // fixme: expose this `Button` text as a formLabel property, too
3244                // and provide similar access to a `fa-` icon default.
3245                template.push(
3246                    '<div class="medium-editor-toolbar-form-row">',
3247                    '<input type="checkbox" class="medium-editor-toolbar-anchor-button">',
3248                    '<label>',
3249                    this.customClassOptionText,
3250                    '</label>',
3251                    '</div>'
3252                );
3253            }
3254
3255            return template.join('');
3256
3257        },
3258
3259        // Used by medium-editor when the default toolbar is to be displayed
3260        isDisplayed: function () {
3261            return this.getForm().style.display === 'block';
3262        },
3263
3264        hideForm: function () {
3265            this.getForm().style.display = 'none';
3266            this.getInput().value = '';
3267        },
3268
3269        showForm: function (opts) {
3270            var input = this.getInput(),
3271                targetCheckbox = this.getAnchorTargetCheckbox(),
3272                buttonCheckbox = this.getAnchorButtonCheckbox();
3273
3274            opts = opts || { url: '' };
3275            // TODO: This is for backwards compatability
3276            // We don't need to support the 'string' argument in 6.0.0
3277            if (typeof opts === 'string') {
3278                opts = {
3279                    url: opts
3280                };
3281            }
3282
3283            this.base.saveSelection();
3284            this.hideToolbarDefaultActions();
3285            this.getForm().style.display = 'block';
3286            this.setToolbarPosition();
3287
3288            input.value = opts.url;
3289            input.focus();
3290
3291            // If we have a target checkbox, we want it to be checked/unchecked
3292            // based on whether the existing link has target=_blank
3293            if (targetCheckbox) {
3294                targetCheckbox.checked = opts.target === '_blank';
3295            }
3296
3297            // If we have a custom class checkbox, we want it to be checked/unchecked
3298            // based on whether an existing link already has the class
3299            if (buttonCheckbox) {
3300                var classList = opts.buttonClass ? opts.buttonClass.split(' ') : [];
3301                buttonCheckbox.checked = (classList.indexOf(this.customClassOption) !== -1);
3302            }
3303        },
3304
3305        // Called by core when tearing down medium-editor (destroy)
3306        destroy: function () {
3307            if (!this.form) {
3308                return false;
3309            }
3310
3311            if (this.form.parentNode) {
3312                this.form.parentNode.removeChild(this.form);
3313            }
3314
3315            delete this.form;
3316        },
3317
3318        // core methods
3319
3320        getFormOpts: function () {
3321            // no notion of private functions? wanted `_getFormOpts`
3322            var targetCheckbox = this.getAnchorTargetCheckbox(),
3323                buttonCheckbox = this.getAnchorButtonCheckbox(),
3324                opts = {
3325                    url: this.getInput().value
3326                };
3327
3328            if (this.linkValidation) {
3329                opts.url = this.checkLinkFormat(opts.url);
3330            }
3331
3332            opts.target = '_self';
3333            if (targetCheckbox && targetCheckbox.checked) {
3334                opts.target = '_blank';
3335            }
3336
3337            if (buttonCheckbox && buttonCheckbox.checked) {
3338                opts.buttonClass = this.customClassOption;
3339            }
3340
3341            return opts;
3342        },
3343
3344        doFormSave: function () {
3345            var opts = this.getFormOpts();
3346            this.completeFormSave(opts);
3347        },
3348
3349        completeFormSave: function (opts) {
3350            this.base.restoreSelection();
3351            this.execAction(this.action, opts);
3352            this.base.checkSelection();
3353        },
3354
3355        checkLinkFormat: function (value) {
3356            var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
3357            return (re.test(value) ? '' : 'http://') + value;
3358        },
3359
3360        doFormCancel: function () {
3361            this.base.restoreSelection();
3362            this.base.checkSelection();
3363        },
3364
3365        // form creation and event handling
3366        attachFormEvents: function (form) {
3367            var close = form.querySelector('.medium-editor-toolbar-close'),
3368                save = form.querySelector('.medium-editor-toolbar-save'),
3369                input = form.querySelector('.medium-editor-toolbar-input');
3370
3371            // Handle clicks on the form itself
3372            this.on(form, 'click', this.handleFormClick.bind(this));
3373
3374            // Handle typing in the textbox
3375            this.on(input, 'keyup', this.handleTextboxKeyup.bind(this));
3376
3377            // Handle close button clicks
3378            this.on(close, 'click', this.handleCloseClick.bind(this));
3379
3380            // Handle save button clicks (capture)
3381            this.on(save, 'click', this.handleSaveClick.bind(this), true);
3382
3383        },
3384
3385        createForm: function () {
3386            var doc = this.document,
3387                form = doc.createElement('div');
3388
3389            // Anchor Form (div)
3390            form.className = 'medium-editor-toolbar-form';
3391            form.id = 'medium-editor-toolbar-form-anchor-' + this.getEditorId();
3392            form.innerHTML = this.getTemplate();
3393            this.attachFormEvents(form);
3394
3395            return form;
3396        },
3397
3398        getInput: function () {
3399            return this.getForm().querySelector('input.medium-editor-toolbar-input');
3400        },
3401
3402        getAnchorTargetCheckbox: function () {
3403            return this.getForm().querySelector('.medium-editor-toolbar-anchor-target');
3404        },
3405
3406        getAnchorButtonCheckbox: function () {
3407            return this.getForm().querySelector('.medium-editor-toolbar-anchor-button');
3408        },
3409
3410        handleTextboxKeyup: function (event) {
3411            // For ENTER -> create the anchor
3412            if (event.keyCode === MediumEditor.util.keyCode.ENTER) {
3413                event.preventDefault();
3414                this.doFormSave();
3415                return;
3416            }
3417
3418            // For ESCAPE -> close the form
3419            if (event.keyCode === MediumEditor.util.keyCode.ESCAPE) {
3420                event.preventDefault();
3421                this.doFormCancel();
3422            }
3423        },
3424
3425        handleFormClick: function (event) {
3426            // make sure not to hide form when clicking inside the form
3427            event.stopPropagation();
3428        },
3429
3430        handleSaveClick: function (event) {
3431            // Clicking Save -> create the anchor
3432            event.preventDefault();
3433            this.doFormSave();
3434        },
3435
3436        handleCloseClick: function (event) {
3437            // Click Close -> close the form
3438            event.preventDefault();
3439            this.doFormCancel();
3440        }
3441    });
3442
3443    MediumEditor.extensions.anchor = AnchorForm;
3444}());
3445(function () {
3446    'use strict';
3447
3448    var AnchorPreview = MediumEditor.Extension.extend({
3449        name: 'anchor-preview',
3450
3451        // Anchor Preview Options
3452
3453        /* hideDelay: [number]  (previously options.anchorPreviewHideDelay)
3454         * time in milliseconds to show the anchor tag preview after the mouse has left the anchor tag.
3455         */
3456        hideDelay: 500,
3457
3458        /* previewValueSelector: [string]
3459         * the default selector to locate where to put the activeAnchor value in the preview
3460         */
3461        previewValueSelector: 'a',
3462
3463        init: function () {
3464            this.anchorPreview = this.createPreview();
3465
3466            this.getEditorOption('elementsContainer').appendChild(this.anchorPreview);
3467
3468            this.attachToEditables();
3469        },
3470
3471        getPreviewElement: function () {
3472            return this.anchorPreview;
3473        },
3474
3475        createPreview: function () {
3476            var el = this.document.createElement('div');
3477
3478            el.id = 'medium-editor-anchor-preview-' + this.getEditorId();
3479            el.className = 'medium-editor-anchor-preview';
3480            el.innerHTML = this.getTemplate();
3481
3482            this.on(el, 'click', this.handleClick.bind(this));
3483
3484            return el;
3485        },
3486
3487        getTemplate: function () {
3488            return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
3489                '    <a class="medium-editor-toolbar-anchor-preview-inner"></a>' +
3490                '</div>';
3491        },
3492
3493        destroy: function () {
3494            if (this.anchorPreview) {
3495                if (this.anchorPreview.parentNode) {
3496                    this.anchorPreview.parentNode.removeChild(this.anchorPreview);
3497                }
3498                delete this.anchorPreview;
3499            }
3500        },
3501
3502        hidePreview: function () {
3503            this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
3504            this.activeAnchor = null;
3505        },
3506
3507        showPreview: function (anchorEl) {
3508            if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active') ||
3509                    anchorEl.getAttribute('data-disable-preview')) {
3510                return true;
3511            }
3512
3513            if (this.previewValueSelector) {
3514                this.anchorPreview.querySelector(this.previewValueSelector).textContent = anchorEl.attributes.href.value;
3515                this.anchorPreview.querySelector(this.previewValueSelector).href = anchorEl.attributes.href.value;
3516            }
3517
3518            this.anchorPreview.classList.add('medium-toolbar-arrow-over');
3519            this.anchorPreview.classList.remove('medium-toolbar-arrow-under');
3520
3521            if (!this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
3522                this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
3523            }
3524
3525            this.activeAnchor = anchorEl;
3526
3527            this.positionPreview();
3528            this.attachPreviewHandlers();
3529
3530            return this;
3531        },
3532
3533        positionPreview: function (activeAnchor) {
3534            activeAnchor = activeAnchor || this.activeAnchor;
3535            var buttonHeight = this.anchorPreview.offsetHeight,
3536                boundary = activeAnchor.getBoundingClientRect(),
3537                middleBoundary = (boundary.left + boundary.right) / 2,
3538                diffLeft = this.diffLeft,
3539                diffTop = this.diffTop,
3540                halfOffsetWidth,
3541                defaultLeft;
3542
3543            halfOffsetWidth = this.anchorPreview.offsetWidth / 2;
3544            var toolbarExtension = this.base.getExtensionByName('toolbar');
3545            if (toolbarExtension) {
3546                diffLeft = toolbarExtension.diffLeft;
3547                diffTop = toolbarExtension.diffTop;
3548            }
3549            defaultLeft = diffLeft - halfOffsetWidth;
3550
3551            this.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - diffTop + this.window.pageYOffset - this.anchorPreview.offsetHeight) + 'px';
3552            if (middleBoundary < halfOffsetWidth) {
3553                this.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
3554            } else if ((this.window.innerWidth - middleBoundary) < halfOffsetWidth) {
3555                this.anchorPreview.style.left = this.window.innerWidth + defaultLeft - halfOffsetWidth + 'px';
3556            } else {
3557                this.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
3558            }
3559        },
3560
3561        attachToEditables: function () {
3562            this.subscribe('editableMouseover', this.handleEditableMouseover.bind(this));
3563        },
3564
3565        handleClick: function (event) {
3566            var anchorExtension = this.base.getExtensionByName('anchor'),
3567                activeAnchor = this.activeAnchor;
3568
3569            if (anchorExtension && activeAnchor) {
3570                event.preventDefault();
3571
3572                this.base.selectElement(this.activeAnchor);
3573
3574                // Using setTimeout + delay because:
vendor: 30,834 bytes, lines 3575-4329
3575                // We may actually be displaying the anchor form, which should be controlled by delay
3576                this.base.delay(function () {
3577                    if (activeAnchor) {
3578                        var opts = {
3579                            url: activeAnchor.attributes.href.value,
3580                            target: activeAnchor.getAttribute('target'),
3581                            buttonClass: activeAnchor.getAttribute('class')
3582                        };
3583                        anchorExtension.showForm(opts);
3584                        activeAnchor = null;
3585                    }
3586                }.bind(this));
3587            }
3588
3589            this.hidePreview();
3590        },
3591
3592        handleAnchorMouseout: function () {
3593            this.anchorToPreview = null;
3594            this.off(this.activeAnchor, 'mouseout', this.instanceHandleAnchorMouseout);
3595            this.instanceHandleAnchorMouseout = null;
3596        },
3597
3598        handleEditableMouseover: function (event) {
3599            var target = MediumEditor.util.getClosestTag(event.target, 'a');
3600
3601            if (false === target) {
3602                return;
3603            }
3604
3605            // Detect empty href attributes
3606            // The browser will make href="" or href="#top"
3607            // into absolute urls when accessed as event.target.href, so check the html
3608            if (!/href=["']\S+["']/.test(target.outerHTML) || /href=["']#\S+["']/.test(target.outerHTML)) {
3609                return true;
3610            }
3611
3612            // only show when toolbar is not present
3613            var toolbar = this.base.getExtensionByName('toolbar');
3614            if (toolbar && toolbar.isDisplayed && toolbar.isDisplayed()) {
3615                return true;
3616            }
3617
3618            // detach handler for other anchor in case we hovered multiple anchors quickly
3619            if (this.activeAnchor && this.activeAnchor !== target) {
3620                this.detachPreviewHandlers();
3621            }
3622
3623            this.anchorToPreview = target;
3624
3625            this.instanceHandleAnchorMouseout = this.handleAnchorMouseout.bind(this);
3626            this.on(this.anchorToPreview, 'mouseout', this.instanceHandleAnchorMouseout);
3627            // Using setTimeout + delay because:
3628            // - We're going to show the anchor preview according to the configured delay
3629            //   if the mouse has not left the anchor tag in that time
3630            this.base.delay(function () {
3631                if (this.anchorToPreview) {
3632                    this.showPreview(this.anchorToPreview);
3633                }
3634            }.bind(this));
3635        },
3636
3637        handlePreviewMouseover: function () {
3638            this.lastOver = (new Date()).getTime();
3639            this.hovering = true;
3640        },
3641
3642        handlePreviewMouseout: function (event) {
3643            if (!event.relatedTarget || !/anchor-preview/.test(event.relatedTarget.className)) {
3644                this.hovering = false;
3645            }
3646        },
3647
3648        updatePreview: function () {
3649            if (this.hovering) {
3650                return true;
3651            }
3652            var durr = (new Date()).getTime() - this.lastOver;
3653            if (durr > this.hideDelay) {
3654                // hide the preview 1/2 second after mouse leaves the link
3655                this.detachPreviewHandlers();
3656            }
3657        },
3658
3659        detachPreviewHandlers: function () {
3660            // cleanup
3661            clearInterval(this.intervalTimer);
3662            if (this.instanceHandlePreviewMouseover) {
3663                this.off(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);
3664                this.off(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);
3665                if (this.activeAnchor) {
3666                    this.off(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);
3667                    this.off(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);
3668                }
3669            }
3670
3671            this.hidePreview();
3672
3673            this.hovering = this.instanceHandlePreviewMouseover = this.instanceHandlePreviewMouseout = null;
3674        },
3675
3676        // TODO: break up method and extract out handlers
3677        attachPreviewHandlers: function () {
3678            this.lastOver = (new Date()).getTime();
3679            this.hovering = true;
3680
3681            this.instanceHandlePreviewMouseover = this.handlePreviewMouseover.bind(this);
3682            this.instanceHandlePreviewMouseout = this.handlePreviewMouseout.bind(this);
3683
3684            this.intervalTimer = setInterval(this.updatePreview.bind(this), 200);
3685
3686            this.on(this.anchorPreview, 'mouseover', this.instanceHandlePreviewMouseover);
3687            this.on(this.anchorPreview, 'mouseout', this.instanceHandlePreviewMouseout);
3688            this.on(this.activeAnchor, 'mouseover', this.instanceHandlePreviewMouseover);
3689            this.on(this.activeAnchor, 'mouseout', this.instanceHandlePreviewMouseout);
3690        }
3691    });
3692
3693    MediumEditor.extensions.anchorPreview = AnchorPreview;
3694}());
3695
3696(function () {
3697    'use strict';
3698
3699    var WHITESPACE_CHARS,
3700        KNOWN_TLDS_FRAGMENT,
3701        LINK_REGEXP_TEXT,
3702        KNOWN_TLDS_REGEXP;
3703
3704    WHITESPACE_CHARS = [' ', '\t', '\n', '\r', '\u00A0', '\u2000', '\u2001', '\u2002', '\u2003',
3705                                    '\u2028', '\u2029'];
3706    KNOWN_TLDS_FRAGMENT = 'com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|' +
3707        '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|' +
3708        '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|' +
3709        '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|' +
3710        '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|' +
3711        '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|' +
3712        '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|' +
3713        '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';
3714
3715    LINK_REGEXP_TEXT =
3716        '(' +
3717        // Version of Gruber URL Regexp optimized for JS: http://stackoverflow.com/a/17733640
3718        '((?:(https?://|ftps?://|nntp://)|www\\d{0,3}[.]|[a-z0-9.\\-]+[.](' + KNOWN_TLDS_FRAGMENT + ')\\\/)\\S+(?:[^\\s`!\\[\\]{};:\'\".,?\u00AB\u00BB\u201C\u201D\u2018\u2019]))' +
3719        // Addition to above Regexp to support bare domains/one level subdomains with common non-i18n TLDs and without www prefix:
3720        ')|(([a-z0-9\\-]+\\.)?[a-z0-9\\-]+\\.(' + KNOWN_TLDS_FRAGMENT + '))';
3721
3722    KNOWN_TLDS_REGEXP = new RegExp('^(' + KNOWN_TLDS_FRAGMENT + ')$', 'i');
3723
3724    function nodeIsNotInsideAnchorTag(node) {
3725        return !MediumEditor.util.getClosestTag(node, 'a');
3726    }
3727
3728    var AutoLink = MediumEditor.Extension.extend({
3729        init: function () {
3730            MediumEditor.Extension.prototype.init.apply(this, arguments);
3731
3732            this.disableEventHandling = false;
3733            this.subscribe('editableKeypress', this.onKeypress.bind(this));
3734            this.subscribe('editableBlur', this.onBlur.bind(this));
3735            // MS IE has it's own auto-URL detect feature but ours is better in some ways. Be consistent.
3736            this.document.execCommand('AutoUrlDetect', false, false);
3737        },
3738
3739        destroy: function () {
3740            // Turn AutoUrlDetect back on
3741            if (this.document.queryCommandSupported('AutoUrlDetect')) {
3742                this.document.execCommand('AutoUrlDetect', false, true);
3743            }
3744        },
3745
3746        onBlur: function (blurEvent, editable) {
3747            this.performLinking(editable);
3748        },
3749
3750        onKeypress: function (keyPressEvent) {
3751            if (this.disableEventHandling) {
3752                return;
3753            }
3754
3755            if (MediumEditor.util.isKey(keyPressEvent, [MediumEditor.util.keyCode.SPACE, MediumEditor.util.keyCode.ENTER])) {
3756                clearTimeout(this.performLinkingTimeout);
3757                // Saving/restoring the selection in the middle of a keypress doesn't work well...
3758                this.performLinkingTimeout = setTimeout(function () {
3759                    try {
3760                        var sel = this.base.exportSelection();
3761                        if (this.performLinking(keyPressEvent.target)) {
3762                            // pass true for favorLaterSelectionAnchor - this is needed for links at the end of a
3763                            // paragraph in MS IE, or MS IE causes the link to be deleted right after adding it.
3764                            this.base.importSelection(sel, true);
3765                        }
3766                    } catch (e) {
3767                        if (window.console) {
3768                            window.console.error('Failed to perform linking', e);
3769                        }
3770                        this.disableEventHandling = true;
3771                    }
3772                }.bind(this), 0);
3773            }
3774        },
3775
3776        performLinking: function (contenteditable) {
3777            // Perform linking on a paragraph level basis as otherwise the detection can wrongly find the end
3778            // of one paragraph and the beginning of another paragraph to constitute a link, such as a paragraph ending
3779            // "link." and the next paragraph beginning with "my" is interpreted into "link.my" and the code tries to create
3780            // a link across blockElements - which doesn't work and is terrible.
3781            // (Medium deletes the spaces/returns between P tags so the textContent ends up without paragraph spacing)
3782            var blockElements = MediumEditor.util.splitByBlockElements(contenteditable),
3783                documentModified = false;
3784            if (blockElements.length === 0) {
3785                blockElements = [contenteditable];
3786            }
3787            for (var i = 0; i < blockElements.length; i++) {
3788                documentModified = this.removeObsoleteAutoLinkSpans(blockElements[i]) || documentModified;
3789                documentModified = this.performLinkingWithinElement(blockElements[i]) || documentModified;
3790            }
3791            return documentModified;
3792        },
3793
3794        removeObsoleteAutoLinkSpans: function (element) {
3795            if (!element || element.nodeType === 3) {
3796                return false;
3797            }
3798
3799            var spans = element.querySelectorAll('span[data-auto-link="true"]'),
3800                documentModified = false;
3801
3802            for (var i = 0; i < spans.length; i++) {
3803                var textContent = spans[i].textContent;
3804                if (textContent.indexOf('://') === -1) {
3805                    textContent = MediumEditor.util.ensureUrlHasProtocol(textContent);
3806                }
3807                if (spans[i].getAttribute('data-href') !== textContent && nodeIsNotInsideAnchorTag(spans[i])) {
3808                    documentModified = true;
3809                    var trimmedTextContent = textContent.replace(/\s+$/, '');
3810                    if (spans[i].getAttribute('data-href') === trimmedTextContent) {
3811                        var charactersTrimmed = textContent.length - trimmedTextContent.length,
3812                            subtree = MediumEditor.util.splitOffDOMTree(spans[i], this.splitTextBeforeEnd(spans[i], charactersTrimmed));
3813                        spans[i].parentNode.insertBefore(subtree, spans[i].nextSibling);
3814                    } else {
3815                        // Some editing has happened to the span, so just remove it entirely. The user can put it back
3816                        // around just the href content if they need to prevent it from linking
3817                        MediumEditor.util.unwrap(spans[i], this.document);
3818                    }
3819                }
3820            }
3821            return documentModified;
3822        },
3823
3824        splitTextBeforeEnd: function (element, characterCount) {
3825            var treeWalker = this.document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false),
3826                lastChildNotExhausted = true;
3827
3828            // Start the tree walker at the last descendant of the span
3829            while (lastChildNotExhausted) {
3830                lastChildNotExhausted = treeWalker.lastChild() !== null;
3831            }
3832
3833            var currentNode,
3834                currentNodeValue,
3835                previousNode;
3836            while (characterCount > 0 && previousNode !== null) {
3837                currentNode = treeWalker.currentNode;
3838                currentNodeValue = currentNode.nodeValue;
3839                if (currentNodeValue.length > characterCount) {
3840                    previousNode = currentNode.splitText(currentNodeValue.length - characterCount);
3841                    characterCount = 0;
3842                } else {
3843                    previousNode = treeWalker.previousNode();
3844                    characterCount -= currentNodeValue.length;
3845                }
3846            }
3847            return previousNode;
3848        },
3849
3850        performLinkingWithinElement: function (element) {
3851            var matches = this.findLinkableText(element),
3852                linkCreated = false;
3853
3854            for (var matchIndex = 0; matchIndex < matches.length; matchIndex++) {
3855                var matchingTextNodes = MediumEditor.util.findOrCreateMatchingTextNodes(this.document, element,
3856                        matches[matchIndex]);
3857                if (this.shouldNotLink(matchingTextNodes)) {
3858                    continue;
3859                }
3860                this.createAutoLink(matchingTextNodes, matches[matchIndex].href);
3861            }
3862            return linkCreated;
3863        },
3864
3865        shouldNotLink: function (textNodes) {
3866            var shouldNotLink = false;
3867            for (var i = 0; i < textNodes.length && shouldNotLink === false; i++) {
3868                // Do not link if the text node is either inside an anchor or inside span[data-auto-link]
3869                shouldNotLink = !!MediumEditor.util.traverseUp(textNodes[i], function (node) {
3870                    return node.nodeName.toLowerCase() === 'a' ||
3871                        (node.getAttribute && node.getAttribute('data-auto-link') === 'true');
3872                });
3873            }
3874            return shouldNotLink;
3875        },
3876
3877        findLinkableText: function (contenteditable) {
3878            var linkRegExp = new RegExp(LINK_REGEXP_TEXT, 'gi'),
3879                textContent = contenteditable.textContent,
3880                match = null,
3881                matches = [];
3882
3883            while ((match = linkRegExp.exec(textContent)) !== null) {
3884                var matchOk = true,
3885                    matchEnd = match.index + match[0].length;
3886                // If the regexp detected something as a link that has text immediately preceding/following it, bail out.
3887                matchOk = (match.index === 0 || WHITESPACE_CHARS.indexOf(textContent[match.index - 1]) !== -1) &&
3888                    (matchEnd === textContent.length || WHITESPACE_CHARS.indexOf(textContent[matchEnd]) !== -1);
3889                // If the regexp detected a bare domain that doesn't use one of our expected TLDs, bail out.
3890                matchOk = matchOk && (match[0].indexOf('/') !== -1 ||
3891                    KNOWN_TLDS_REGEXP.test(match[0].split('.').pop().split('?').shift()));
3892
3893                if (matchOk) {
3894                    matches.push({
3895                        href: match[0],
3896                        start: match.index,
3897                        end: matchEnd
3898                    });
3899                }
3900            }
3901            return matches;
3902        },
3903
3904        createAutoLink: function (textNodes, href) {
3905            href = MediumEditor.util.ensureUrlHasProtocol(href);
3906            var anchor = MediumEditor.util.createLink(this.document, textNodes, href, this.getEditorOption('targetBlank') ? '_blank' : null),
3907                span = this.document.createElement('span');
3908            span.setAttribute('data-auto-link', 'true');
3909            span.setAttribute('data-href', href);
3910            anchor.insertBefore(span, anchor.firstChild);
3911            while (anchor.childNodes.length > 1) {
3912                span.appendChild(anchor.childNodes[1]);
3913            }
3914        }
3915
3916    });
3917
3918    MediumEditor.extensions.autoLink = AutoLink;
3919}());
3920(function () {
3921    'use strict';
3922
3923    var CLASS_DRAG_OVER = 'medium-editor-dragover';
3924
3925    function clearClassNames(element) {
3926        var editable = MediumEditor.util.getContainerEditorElement(element),
3927            existing = Array.prototype.slice.call(editable.parentElement.querySelectorAll('.' + CLASS_DRAG_OVER));
3928
3929        existing.forEach(function (el) {
3930            el.classList.remove(CLASS_DRAG_OVER);
3931        });
3932    }
3933
3934    var FileDragging = MediumEditor.Extension.extend({
3935        name: 'fileDragging',
3936
3937        allowedTypes: ['image'],
3938
3939        init: function () {
3940            MediumEditor.Extension.prototype.init.apply(this, arguments);
3941
3942            this.subscribe('editableDrag', this.handleDrag.bind(this));
3943            this.subscribe('editableDrop', this.handleDrop.bind(this));
3944        },
3945
3946        handleDrag: function (event) {
3947            event.preventDefault();
3948            event.dataTransfer.dropEffect = 'copy';
3949
3950            var target = event.target.classList ? event.target : event.target.parentElement;
3951
3952            // Ensure the class gets removed from anything that had it before
3953            clearClassNames(target);
3954
3955            if (event.type === 'dragover') {
3956                target.classList.add(CLASS_DRAG_OVER);
3957            }
3958        },
3959
3960        handleDrop: function (event) {
3961            // Prevent file from opening in the current window
3962            event.preventDefault();
3963            event.stopPropagation();
3964
3965            // IE9 does not support the File API, so prevent file from opening in the window
3966            // but also don't try to actually get the file
3967            if (event.dataTransfer.files) {
3968                Array.prototype.slice.call(event.dataTransfer.files).forEach(function (file) {
3969                    if (this.isAllowedFile(file)) {
3970                        if (file.type.match('image')) {
3971                            this.insertImageFile(file);
3972                        }
3973                    }
3974                }, this);
3975            }
3976
3977            // Make sure we remove our class from everything
3978            clearClassNames(event.target);
3979        },
3980
3981        isAllowedFile: function (file) {
3982            return this.allowedTypes.some(function (fileType) {
3983                return !!file.type.match(fileType);
3984            });
3985        },
3986
3987        insertImageFile: function (file) {
3988            var fileReader = new FileReader();
3989            fileReader.readAsDataURL(file);
3990
3991            var id = 'medium-img-' + (+new Date());
3992            MediumEditor.util.insertHTMLCommand(this.document, '<img class="medium-editor-image-loading" id="' + id + '" />');
3993
3994            fileReader.onload = function () {
3995                var img = this.document.getElementById(id);
3996                if (img) {
3997                    img.removeAttribute('id');
3998                    img.removeAttribute('class');
3999                    img.src = fileReader.result;
4000                }
4001            }.bind(this);
4002        }
4003    });
4004
4005    MediumEditor.extensions.fileDragging = FileDragging;
4006}());
4007
4008(function () {
4009    'use strict';
4010
4011    var KeyboardCommands = MediumEditor.Extension.extend({
4012        name: 'keyboard-commands',
4013
4014        /* KeyboardCommands Options */
4015
4016        /* commands: [Array]
4017         * Array of objects describing each command and the combination of keys that will trigger it
4018         * Required for each object:
4019         *   command [String] (argument passed to editor.execAction())
4020         *   key [String] (keyboard character that triggers this command)
4021         *   meta [boolean] (whether the ctrl/meta key has to be active or inactive)
4022         *   shift [boolean] (whether the shift key has to be active or inactive)
4023         *   alt [boolean] (whether the alt key has to be active or inactive)
4024         */
4025        commands: [
4026            {
4027                command: 'bold',
4028                key: 'B',
4029                meta: true,
4030                shift: false,
4031                alt: false
4032            },
4033            {
4034                command: 'italic',
4035                key: 'I',
4036                meta: true,
4037                shift: false,
4038                alt: false
4039            },
4040            {
4041                command: 'underline',
4042                key: 'U',
4043                meta: true,
4044                shift: false,
4045                alt: false
4046            }
4047        ],
4048
4049        init: function () {
4050            MediumEditor.Extension.prototype.init.apply(this, arguments);
4051
4052            this.subscribe('editableKeydown', this.handleKeydown.bind(this));
4053            this.keys = {};
4054            this.commands.forEach(function (command) {
4055                var keyCode = command.key.charCodeAt(0);
4056                if (!this.keys[keyCode]) {
4057                    this.keys[keyCode] = [];
4058                }
4059                this.keys[keyCode].push(command);
4060            }, this);
4061        },
4062
4063        handleKeydown: function (event) {
4064            var keyCode = MediumEditor.util.getKeyCode(event);
4065            if (!this.keys[keyCode]) {
4066                return;
4067            }
4068
4069            var isMeta = MediumEditor.util.isMetaCtrlKey(event),
4070                isShift = !!event.shiftKey,
4071                isAlt = !!event.altKey;
4072
4073            this.keys[keyCode].forEach(function (data) {
4074                if (data.meta === isMeta &&
4075                    data.shift === isShift &&
4076                    (data.alt === isAlt ||
4077                     undefined === data.alt)) { // TODO deprecated: remove check for undefined === data.alt when jumping to 6.0.0
4078                    event.preventDefault();
4079                    event.stopPropagation();
4080
4081                    // command can be false so the shortcut is just disabled
4082                    if (false !== data.command) {
4083                        this.execAction(data.command);
4084                    }
4085                }
4086            }, this);
4087        }
4088    });
4089
4090    MediumEditor.extensions.keyboardCommands = KeyboardCommands;
4091}());
4092
4093(function () {
4094    'use strict';
4095
4096    var FontSizeForm = MediumEditor.extensions.form.extend({
4097
4098        name: 'fontsize',
4099        action: 'fontSize',
4100        aria: 'increase/decrease font size',
4101        contentDefault: '&#xB1;', // ±
4102        contentFA: '<i class="fa fa-text-height"></i>',
4103
4104        init: function () {
4105            MediumEditor.extensions.form.prototype.init.apply(this, arguments);
4106        },
4107
4108        // Called when the button the toolbar is clicked
4109        // Overrides ButtonExtension.handleClick
4110        handleClick: function (event) {
4111            event.preventDefault();
4112            event.stopPropagation();
4113
4114            if (!this.isDisplayed()) {
4115                // Get fontsize of current selection (convert to string since IE returns this as number)
4116                var fontSize = this.document.queryCommandValue('fontSize') + '';
4117                this.showForm(fontSize);
4118            }
4119
4120            return false;
4121        },
4122
4123        // Called by medium-editor to append form to the toolbar
4124        getForm: function () {
4125            if (!this.form) {
4126                this.form = this.createForm();
4127            }
4128            return this.form;
4129        },
4130
4131        // Used by medium-editor when the default toolbar is to be displayed
4132        isDisplayed: function () {
4133            return this.getForm().style.display === 'block';
4134        },
4135
4136        hideForm: function () {
4137            this.getForm().style.display = 'none';
4138            this.getInput().value = '';
4139        },
4140
4141        showForm: function (fontSize) {
4142            var input = this.getInput();
4143
4144            this.base.saveSelection();
4145            this.hideToolbarDefaultActions();
4146            this.getForm().style.display = 'block';
4147            this.setToolbarPosition();
4148
4149            input.value = fontSize || '';
4150            input.focus();
4151        },
4152
4153        // Called by core when tearing down medium-editor (destroy)
4154        destroy: function () {
4155            if (!this.form) {
4156                return false;
4157            }
4158
4159            if (this.form.parentNode) {
4160                this.form.parentNode.removeChild(this.form);
4161            }
4162
4163            delete this.form;
4164        },
4165
4166        // core methods
4167
4168        doFormSave: function () {
4169            this.base.restoreSelection();
4170            this.base.checkSelection();
4171        },
4172
4173        doFormCancel: function () {
4174            this.base.restoreSelection();
4175            this.clearFontSize();
4176            this.base.checkSelection();
4177        },
4178
4179        // form creation and event handling
4180        createForm: function () {
4181            var doc = this.document,
4182                form = doc.createElement('div'),
4183                input = doc.createElement('input'),
4184                close = doc.createElement('a'),
4185                save = doc.createElement('a');
4186
4187            // Font Size Form (div)
4188            form.className = 'medium-editor-toolbar-form';
4189            form.id = 'medium-editor-toolbar-form-fontsize-' + this.getEditorId();
4190
4191            // Handle clicks on the form itself
4192            this.on(form, 'click', this.handleFormClick.bind(this));
4193
4194            // Add font size slider
4195            input.setAttribute('type', 'range');
4196            input.setAttribute('min', '1');
4197            input.setAttribute('max', '7');
4198            input.className = 'medium-editor-toolbar-input';
4199            form.appendChild(input);
4200
4201            // Handle typing in the textbox
4202            this.on(input, 'change', this.handleSliderChange.bind(this));
4203
4204            // Add save buton
4205            save.setAttribute('href', '#');
4206            save.className = 'medium-editor-toobar-save';
4207            save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
4208                             '<i class="fa fa-check"></i>' :
4209                             '&#10003;';
4210            form.appendChild(save);
4211
4212            // Handle save button clicks (capture)
4213            this.on(save, 'click', this.handleSaveClick.bind(this), true);
4214
4215            // Add close button
4216            close.setAttribute('href', '#');
4217            close.className = 'medium-editor-toobar-close';
4218            close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ?
4219                              '<i class="fa fa-times"></i>' :
4220                              '&times;';
4221            form.appendChild(close);
4222
4223            // Handle close button clicks
4224            this.on(close, 'click', this.handleCloseClick.bind(this));
4225
4226            return form;
4227        },
4228
4229        getInput: function () {
4230            return this.getForm().querySelector('input.medium-editor-toolbar-input');
4231        },
4232
4233        clearFontSize: function () {
4234            MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) {
4235                if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) {
4236                    el.removeAttribute('size');
4237                }
4238            });
4239        },
4240
4241        handleSliderChange: function () {
4242            var size = this.getInput().value;
4243            if (size === '4') {
4244                this.clearFontSize();
4245            } else {
4246                this.execAction('fontSize', { size: size });
4247            }
4248        },
4249
4250        handleFormClick: function (event) {
4251            // make sure not to hide form when clicking inside the form
4252            event.stopPropagation();
4253        },
4254
4255        handleSaveClick: function (event) {
4256            // Clicking Save -> create the font size
4257            event.preventDefault();
4258            this.doFormSave();
4259        },
4260
4261        handleCloseClick: function (event) {
4262            // Click Close -> close the form
4263            event.preventDefault();
4264            this.doFormCancel();
4265        }
4266    });
4267
4268    MediumEditor.extensions.fontSize = FontSizeForm;
4269}());
4270(function () {
4271    'use strict';
4272    /*jslint regexp: true*/
4273    /*
4274        jslint does not allow character negation, because the negation
4275        will not match any unicode characters. In the regexes in this
4276        block, negation is used specifically to match the end of an html
4277        tag, and in fact unicode characters *should* be allowed.
4278    */
4279    function createReplacements() {
4280        return [
4281            // replace two bogus tags that begin pastes from google docs
4282            [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ''],
4283            [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ''],
4284
4285             // un-html spaces and newlines inserted by OS X
4286            [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
4287            [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
4288
4289            // replace google docs italics+bold with a span to be replaced once the html is inserted
4290            [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
4291
4292            // replace google docs italics with a span to be replaced once the html is inserted
4293            [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
4294
4295            //[replace google docs bolds with a span to be replaced once the html is inserted
4296            [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
4297
4298             // replace manually entered b/i/a tags with real ones
4299            [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
4300
4301             // replace manually a tags with real ones, converting smart-quotes from google docs
4302            [new RegExp(/&lt;a(?:(?!href).)+href=(?:&quot;|&rdquo;|&ldquo;|"|“|”)(((?!&quot;|&rdquo;|&ldquo;|"|“|”).)*)(?:&quot;|&rdquo;|&ldquo;|"|“|”)(?:(?!&gt;).)*&gt;/gi), '<a href="$1">'],
4303
4304            // Newlines between paragraphs in html have no syntactic value,
4305            // but then have a tendency to accidentally become additional paragraphs down the line
4306            [new RegExp(/<\/p>\n+/gi), '</p>'],
4307            [new RegExp(/\n+<p/gi), '<p'],
4308
4309            // Microsoft Word makes these odd tags, like <o:p></o:p>
4310            [new RegExp(/<\/?o:[a-z]*>/gi), ''],
4311
4312            // cleanup comments added by Chrome when pasting html
4313            ['<!--EndFragment-->', ''],
4314            ['<!--StartFragment-->', '']
4315        ];
4316    }
4317    /*jslint regexp: false*/
4318
4319    var PasteHandler = MediumEditor.Extension.extend({
4320        /* Paste Options */
4321
4322        /* forcePlainText: [boolean]
4323         * Forces pasting as plain text.
4324         */
4325        forcePlainText: true,
4326
4327        /* cleanPastedHTML: [boolean]
4328         * cleans pasted content from different sources, like google docs etc.
4329         */
4330        cleanPastedHTML: false,
4331
4332        /* cleanReplacements: [Array]
4333         * custom pairs (2 element arrays) of RegExp and replacement text to use during paste when
4334         * __forcePlainText__ or __cleanPastedHTML__ are `true` OR when calling `cleanPaste(text)` helper method.
4335         */
4336        cleanReplacements: [],
4337
4338        /* cleanAttrs:: [Array]
4339         * list of element attributes to remove during paste when __cleanPastedHTML__ is `true` or when
4340         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
4341         */
4342        cleanAttrs: ['class', 'style', 'dir'],
4343
4344        /* cleanTags: [Array]
4345         * list of element tag names to remove during paste when __cleanPastedHTML__ is `true` or when
4346         * calling `cleanPaste(text)` or `pasteHTML(html, options)` helper methods.
4347         */
4348        cleanTags: ['meta'],
4349
4350        init: function () {
4351            MediumEditor.Extension.prototype.init.apply(this, arguments);
4352
4353            if (this.forcePlainText || this.cleanPastedHTML) {
4354                this.subscribe('editablePaste', this.handlePaste.bind(this));
4355            }
4356        },
4357
4358        handlePaste: function (event, element) {
4359            var paragraphs,
4360                html = '',
4361                p,
4362                dataFormatHTML = 'text/html',
4363                dataFormatPlain = 'text/plain',
4364                pastedHTML,
4365                pastedPlain;
4366
4367            if (this.window.clipboardData && event.clipboardData === undefined) {
4368                event.clipboardData = this.window.clipboardData;
4369                // If window.clipboardData exists, but event.clipboardData doesn't exist,
4370                // we're probably in IE. IE only has two possibilities for clipboard
4371                // data format: 'Text' and 'URL'.
4372                //
4373                // Of the two, we want 'Text':
4374                dataFormatHTML = 'Text';
4375                dataFormatPlain = 'Text';
4376            }
4377
4378            if (event.clipboardData &&
4379                    event.clipboardData.getData &&
4380                    !event.defaultPrevented) {
4381                event.preventDefault();
4382
4383                pastedHTML = event.clipboardData.getData(dataFormatHTML);
4384                pastedPlain = event.clipboardData.getData(dataFormatPlain);
4385
4386                if (this.cleanPastedHTML && pastedHTML) {
4387                    return this.cleanPaste(pastedHTML);
4388                }
4389
4390                if (!(this.getEditorOption('disableReturn') || element.getAttribute('data-disable-return'))) {
4391                    paragraphs = pastedPlain.split(/[\r\n]+/g);
4392                    // If there are no \r\n in data, don't wrap in <p>
4393                    if (paragraphs.length > 1) {
4394                        for (p = 0; p < paragraphs.length; p += 1) {
4395                            if (paragraphs[p] !== '') {
4396                                html += '<p>' + MediumEditor.util.htmlEntities(paragraphs[p]) + '</p>';
4397                            }
4398                        }
4399                    } else {
4400                        html = MediumEditor.util.htmlEntities(paragraphs[0]);
4401                    }
4402                } else {
4403                    html = MediumEditor.util.htmlEntities(pastedPlain);
4404                }
4405                MediumEditor.util.insertHTMLCommand(this.document, html);
4406            }
4407        },
4408
4409        cleanPaste: function (text) {
4410            var i, elList, tmp, workEl,
4411                multiline = /<p|<br|<div/.test(text),
4412                replacements = createReplacements().concat(this.cleanReplacements || []);
4413
4414            for (i = 0; i < replacements.length; i += 1) {
4415                text = text.replace(replacements[i][0], replacements[i][1]);
4416            }
4417
4418            if (!multiline) {
4419                return this.pasteHTML(text);
4420            }
4421
4422            // create a temporary div to cleanup block elements
4423            tmp = this.document.createElement('div');
4424
4425            // double br's aren't converted to p tags, but we want paragraphs.
4426            tmp.innerHTML = '<p>' + text.split('<br><br>').join('</p><p>') + '</p>';
4427
4428            // block element cleanup
4429            elList = tmp.querySelectorAll('a,p,div,br');
4430            for (i = 0; i < elList.length; i += 1) {
4431                workEl = elList[i];
4432
4433                // Microsoft Word replaces some spaces with newlines.
4434                // While newlines between block elements are meaningless, newlines within
4435                // elements are sometimes actually spaces.
4436                workEl.innerHTML = workEl.innerHTML.replace(/\n/gi, ' ');
4437
4438                switch (workEl.nodeName.toLowerCase()) {
4439                    case 'p':
4440                    case 'div':
4441                        this.filterCommonBlocks(workEl);
4442                        break;
4443                    case 'br':
4444                        this.filterLineBreak(workEl);
4445                        break;
4446                }
4447            }
4448
4449            this.pasteHTML(tmp.innerHTML);
4450        },
4451
4452        pasteHTML: function (html, options) {
4453            options = MediumEditor.util.defaults({}, options, {
4454                cleanAttrs: this.cleanAttrs,
4455                cleanTags: this.cleanTags
4456            });
4457
4458            var elList, workEl, i, fragmentBody, pasteBlock = this.document.createDocumentFragment();
4459
4460            pasteBlock.appendChild(this.document.createElement('body'));
4461
4462            fragmentBody = pasteBlock.querySelector('body');
4463            fragmentBody.innerHTML = html;
4464
4465            this.cleanupSpans(fragmentBody);
4466
4467            elList = fragmentBody.querySelectorAll('*');
4468            for (i = 0; i < elList.length; i += 1) {
4469                workEl = elList[i];
4470
4471                if ('a' === workEl.nodeName.toLowerCase() && this.getEditorOption('targetBlank')) {
4472                    MediumEditor.util.setTargetBlank(workEl);
4473                }
4474
4475                MediumEditor.util.cleanupAttrs(workEl, options.cleanAttrs);
vendor: 28,916 bytes, lines 4476-5212
4476                MediumEditor.util.cleanupTags(workEl, options.cleanTags);
4477            }
4478
4479            MediumEditor.util.insertHTMLCommand(this.document, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
4480        },
4481
4482        isCommonBlock: function (el) {
4483            return (el && (el.nodeName.toLowerCase() === 'p' || el.nodeName.toLowerCase() === 'div'));
4484        },
4485
4486        filterCommonBlocks: function (el) {
4487            if (/^\s*$/.test(el.textContent) && el.parentNode) {
4488                el.parentNode.removeChild(el);
4489            }
4490        },
4491
4492        filterLineBreak: function (el) {
4493            if (this.isCommonBlock(el.previousElementSibling)) {
4494                // remove stray br's following common block elements
4495                this.removeWithParent(el);
4496            } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
4497                // remove br's just inside open or close tags of a div/p
4498                this.removeWithParent(el);
4499            } else if (el.parentNode && el.parentNode.childElementCount === 1 && el.parentNode.textContent === '') {
4500                // and br's that are the only child of elements other than div/p
4501                this.removeWithParent(el);
4502            }
4503        },
4504
4505        // remove an element, including its parent, if it is the only element within its parent
4506        removeWithParent: function (el) {
4507            if (el && el.parentNode) {
4508                if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
4509                    el.parentNode.parentNode.removeChild(el.parentNode);
4510                } else {
4511                    el.parentNode.removeChild(el);
4512                }
4513            }
4514        },
4515
4516        cleanupSpans: function (containerEl) {
4517            var i,
4518                el,
4519                newEl,
4520                spans = containerEl.querySelectorAll('.replace-with'),
4521                isCEF = function (el) {
4522                    return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
4523                };
4524
4525            for (i = 0; i < spans.length; i += 1) {
4526                el = spans[i];
4527                newEl = this.document.createElement(el.classList.contains('bold') ? 'b' : 'i');
4528
4529                if (el.classList.contains('bold') && el.classList.contains('italic')) {
4530                    // add an i tag as well if this has both italics and bold
4531                    newEl.innerHTML = '<i>' + el.innerHTML + '</i>';
4532                } else {
4533                    newEl.innerHTML = el.innerHTML;
4534                }
4535                el.parentNode.replaceChild(newEl, el);
4536            }
4537
4538            spans = containerEl.querySelectorAll('span');
4539            for (i = 0; i < spans.length; i += 1) {
4540                el = spans[i];
4541
4542                // bail if span is in contenteditable = false
4543                if (MediumEditor.util.traverseUp(el, isCEF)) {
4544                    return false;
4545                }
4546
4547                // remove empty spans, replace others with their contents
4548                MediumEditor.util.unwrap(el, this.document);
4549            }
4550        }
4551    });
4552
4553    MediumEditor.extensions.paste = PasteHandler;
4554}());
4555
4556(function () {
4557    'use strict';
4558
4559    var Placeholder = MediumEditor.Extension.extend({
4560        name: 'placeholder',
4561
4562        /* Placeholder Options */
4563
4564        /* text: [string]
4565         * Text to display in the placeholder
4566         */
4567        text: 'Type your text',
4568
4569        /* hideOnClick: [boolean]
4570         * Should we hide the placeholder on click (true) or when user starts typing (false)
4571         */
4572        hideOnClick: true,
4573
4574        init: function () {
4575            MediumEditor.Extension.prototype.init.apply(this, arguments);
4576
4577            this.initPlaceholders();
4578            this.attachEventHandlers();
4579        },
4580
4581        initPlaceholders: function () {
4582            this.getEditorElements().forEach(function (el) {
4583                if (!el.getAttribute('data-placeholder')) {
4584                    el.setAttribute('data-placeholder', this.text);
4585                }
4586                this.updatePlaceholder(el);
4587            }, this);
4588        },
4589
4590        destroy: function () {
4591            this.getEditorElements().forEach(function (el) {
4592                if (el.getAttribute('data-placeholder') === this.text) {
4593                    el.removeAttribute('data-placeholder');
4594                }
4595            }, this);
4596        },
4597
4598        showPlaceholder: function (el) {
4599            if (el) {
4600                el.classList.add('medium-editor-placeholder');
4601            }
4602        },
4603
4604        hidePlaceholder: function (el) {
4605            if (el) {
4606                el.classList.remove('medium-editor-placeholder');
4607            }
4608        },
4609
4610        updatePlaceholder: function (el, dontShow) {
4611            // If the element has content, hide the placeholder
4612            if (el.querySelector('img, blockquote, ul, ol') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) {
4613                return this.hidePlaceholder(el);
4614            }
4615
4616            if (!dontShow) {
4617                this.showPlaceholder(el);
4618            }
4619        },
4620
4621        attachEventHandlers: function () {
4622            if (this.hideOnClick) {
4623                // For the 'hideOnClick' option, the placeholder should always be hidden on focus
4624                this.subscribe('focus', this.handleFocus.bind(this));
4625            }
4626
4627            // If the editor has content, it should always hide the placeholder
4628            this.subscribe('editableInput', this.handleInput.bind(this));
4629
4630            // When the editor loses focus, check if the placeholder should be visible
4631            this.subscribe('blur', this.handleBlur.bind(this));
4632        },
4633
4634        handleInput: function (event, element) {
4635            // If the placeholder should be hidden on focus and the
4636            // element has focus, don't show the placeholder
4637            var dontShow = this.hideOnClick && (element === this.base.getFocusedElement());
4638
4639            // Editor's content has changed, check if the placeholder should be hidden
4640            this.updatePlaceholder(element, dontShow);
4641        },
4642
4643        handleFocus: function (event, element) {
4644            // Editor has focus, hide the placeholder
4645            this.hidePlaceholder(element);
4646        },
4647
4648        handleBlur: function (event, element) {
4649            // Editor has lost focus, check if the placeholder should be shown
4650            this.updatePlaceholder(element);
4651        }
4652    });
4653
4654    MediumEditor.extensions.placeholder = Placeholder;
4655}());
4656
4657(function () {
4658    'use strict';
4659
4660    var Toolbar = MediumEditor.Extension.extend({
4661        name: 'toolbar',
4662
4663        /* Toolbar Options */
4664
4665        /* align: ['left'|'center'|'right']
4666         * When the __static__ option is true, this aligns the static toolbar
4667         * relative to the medium-editor element.
4668         */
4669        align: 'center',
4670
4671        /* allowMultiParagraphSelection: [boolean]
4672         * enables/disables whether the toolbar should be displayed when
4673         * selecting multiple paragraphs/block elements
4674         */
4675        allowMultiParagraphSelection: true,
4676
4677        /* buttons: [Array]
4678         * the names of the set of buttons to display on the toolbar.
4679         */
4680        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],
4681
4682        /* diffLeft: [Number]
4683         * value in pixels to be added to the X axis positioning of the toolbar.
4684         */
4685        diffLeft: 0,
4686
4687        /* diffTop: [Number]
4688         * value in pixels to be added to the Y axis positioning of the toolbar.
4689         */
4690        diffTop: -10,
4691
4692        /* firstButtonClass: [string]
4693         * CSS class added to the first button in the toolbar.
4694         */
4695        firstButtonClass: 'medium-editor-button-first',
4696
4697        /* lastButtonClass: [string]
4698         * CSS class added to the last button in the toolbar.
4699         */
4700        lastButtonClass: 'medium-editor-button-last',
4701
4702        /* standardizeSelectionStart: [boolean]
4703         * enables/disables standardizing how the beginning of a range is decided
4704         * between browsers whenever the selected text is analyzed for updating toolbar buttons status.
4705         */
4706        standardizeSelectionStart: false,
4707
4708        /* static: [boolean]
4709         * enable/disable the toolbar always displaying in the same location
4710         * relative to the medium-editor element.
4711         */
4712        static: false,
4713
4714        /* sticky: [boolean]
4715         * When the __static__ option is true, this enables/disables the toolbar
4716         * "sticking" to the viewport and staying visible on the screen while
4717         * the page scrolls.
4718         */
4719        sticky: false,
4720
4721        /* updateOnEmptySelection: [boolean]
4722         * When the __static__ option is true, this enables/disables updating
4723         * the state of the toolbar buttons even when the selection is collapsed
4724         * (there is no selection, just a cursor).
4725         */
4726        updateOnEmptySelection: false,
4727
4728        /* relativeContainer: [node]
4729         * appending the toolbar to a given node instead of body
4730         */
4731        relativeContainer: null,
4732
4733        init: function () {
4734            MediumEditor.Extension.prototype.init.apply(this, arguments);
4735
4736            this.initThrottledMethods();
4737
4738            if (!this.relativeContainer) {
4739                this.getEditorOption('elementsContainer').appendChild(this.getToolbarElement());
4740            } else {
4741                this.relativeContainer.appendChild(this.getToolbarElement());
4742            }
4743        },
4744
4745        // Helper method to execute method for every extension, but ignoring the toolbar extension
4746        forEachExtension: function (iterator, context) {
4747            return this.base.extensions.forEach(function (command) {
4748                if (command === this) {
4749                    return;
4750                }
4751                return iterator.apply(context || this, arguments);
4752            }, this);
4753        },
4754
4755        // Toolbar creation/deletion
4756
4757        createToolbar: function () {
4758            var toolbar = this.document.createElement('div');
4759
4760            toolbar.id = 'medium-editor-toolbar-' + this.getEditorId();
4761            toolbar.className = 'medium-editor-toolbar';
4762
4763            if (this.static) {
4764                toolbar.className += ' static-toolbar';
4765            } else if (this.relativeContainer) {
4766                toolbar.className += ' medium-editor-relative-toolbar';
4767            } else {
4768                toolbar.className += ' medium-editor-stalker-toolbar';
4769            }
4770
4771            toolbar.appendChild(this.createToolbarButtons());
4772
4773            // Add any forms that extensions may have
4774            this.forEachExtension(function (extension) {
4775                if (extension.hasForm) {
4776                    toolbar.appendChild(extension.getForm());
4777                }
4778            });
4779
4780            this.attachEventHandlers();
4781
4782            return toolbar;
4783        },
4784
4785        createToolbarButtons: function () {
4786            var ul = this.document.createElement('ul'),
4787                li,
4788                btn,
4789                buttons,
4790                extension,
4791                buttonName,
4792                buttonOpts;
4793
4794            ul.id = 'medium-editor-toolbar-actions' + this.getEditorId();
4795            ul.className = 'medium-editor-toolbar-actions';
4796            ul.style.display = 'block';
4797
4798            this.buttons.forEach(function (button) {
4799                if (typeof button === 'string') {
4800                    buttonName = button;
4801                    buttonOpts = null;
4802                } else {
4803                    buttonName = button.name;
4804                    buttonOpts = button;
4805                }
4806
4807                // If the button already exists as an extension, it'll be returned
4808                // othwerise it'll create the default built-in button
4809                extension = this.base.addBuiltInExtension(buttonName, buttonOpts);
4810
4811                if (extension && typeof extension.getButton === 'function') {
4812                    btn = extension.getButton(this.base);
4813                    li = this.document.createElement('li');
4814                    if (MediumEditor.util.isElement(btn)) {
4815                        li.appendChild(btn);
4816                    } else {
4817                        li.innerHTML = btn;
4818                    }
4819                    ul.appendChild(li);
4820                }
4821            }, this);
4822
4823            buttons = ul.querySelectorAll('button');
4824            if (buttons.length > 0) {
4825                buttons[0].classList.add(this.firstButtonClass);
4826                buttons[buttons.length - 1].classList.add(this.lastButtonClass);
4827            }
4828
4829            return ul;
4830        },
4831
4832        destroy: function () {
4833            if (this.toolbar) {
4834                if (this.toolbar.parentNode) {
4835                    this.toolbar.parentNode.removeChild(this.toolbar);
4836                }
4837                delete this.toolbar;
4838            }
4839        },
4840
4841        // Toolbar accessors
4842
4843        getToolbarElement: function () {
4844            if (!this.toolbar) {
4845                this.toolbar = this.createToolbar();
4846            }
4847
4848            return this.toolbar;
4849        },
4850
4851        getToolbarActionsElement: function () {
4852            return this.getToolbarElement().querySelector('.medium-editor-toolbar-actions');
4853        },
4854
4855        // Toolbar event handlers
4856
4857        initThrottledMethods: function () {
4858            // throttledPositionToolbar is throttled because:
4859            // - It will be called when the browser is resizing, which can fire many times very quickly
4860            // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
4861            this.throttledPositionToolbar = MediumEditor.util.throttle(function () {
4862                if (this.base.isActive) {
4863                    this.positionToolbarIfShown();
4864                }
4865            }.bind(this));
4866        },
4867
4868        attachEventHandlers: function () {
4869            // MediumEditor custom events for when user beings and ends interaction with a contenteditable and its elements
4870            this.subscribe('blur', this.handleBlur.bind(this));
4871            this.subscribe('focus', this.handleFocus.bind(this));
4872
4873            // Updating the state of the toolbar as things change
4874            this.subscribe('editableClick', this.handleEditableClick.bind(this));
4875            this.subscribe('editableKeyup', this.handleEditableKeyup.bind(this));
4876
4877            // Handle mouseup on document for updating the selection in the toolbar
4878            this.on(this.document.documentElement, 'mouseup', this.handleDocumentMouseup.bind(this));
4879
4880            // Add a scroll event for sticky toolbar
4881            if (this.static && this.sticky) {
4882                // On scroll (capture), re-position the toolbar
4883                this.on(this.window, 'scroll', this.handleWindowScroll.bind(this), true);
4884            }
4885
4886            // On resize, re-position the toolbar
4887            this.on(this.window, 'resize', this.handleWindowResize.bind(this));
4888        },
4889
4890        handleWindowScroll: function () {
4891            this.positionToolbarIfShown();
4892        },
4893
4894        handleWindowResize: function () {
4895            this.throttledPositionToolbar();
4896        },
4897
4898        handleDocumentMouseup: function (event) {
4899            // Do not trigger checkState when mouseup fires over the toolbar
4900            if (event &&
4901                    event.target &&
4902                    MediumEditor.util.isDescendant(this.getToolbarElement(), event.target)) {
4903                return false;
4904            }
4905            this.checkState();
4906        },
4907
4908        handleEditableClick: function () {
4909            // Delay the call to checkState to handle bug where selection is empty
4910            // immediately after clicking inside a pre-existing selection
4911            setTimeout(function () {
4912                this.checkState();
4913            }.bind(this), 0);
4914        },
4915
4916        handleEditableKeyup: function () {
4917            this.checkState();
4918        },
4919
4920        handleBlur: function () {
4921            // Kill any previously delayed calls to hide the toolbar
4922            clearTimeout(this.hideTimeout);
4923
4924            // Blur may fire even if we have a selection, so we want to prevent any delayed showToolbar
4925            // calls from happening in this specific case
4926            clearTimeout(this.delayShowTimeout);
4927
4928            // Delay the call to hideToolbar to handle bug with multiple editors on the page at once
4929            this.hideTimeout = setTimeout(function () {
4930                this.hideToolbar();
4931            }.bind(this), 1);
4932        },
4933
4934        handleFocus: function () {
4935            this.checkState();
4936        },
4937
4938        // Hiding/showing toolbar
4939
4940        isDisplayed: function () {
4941            return this.getToolbarElement().classList.contains('medium-editor-toolbar-active');
4942        },
4943
4944        showToolbar: function () {
4945            clearTimeout(this.hideTimeout);
4946            if (!this.isDisplayed()) {
4947                this.getToolbarElement().classList.add('medium-editor-toolbar-active');
4948                this.trigger('showToolbar', {}, this.base.getFocusedElement());
4949            }
4950        },
4951
4952        hideToolbar: function () {
4953            if (this.isDisplayed()) {
4954                this.getToolbarElement().classList.remove('medium-editor-toolbar-active');
4955                this.trigger('hideToolbar', {}, this.base.getFocusedElement());
4956            }
4957        },
4958
4959        isToolbarDefaultActionsDisplayed: function () {
4960            return this.getToolbarActionsElement().style.display === 'block';
4961        },
4962
4963        hideToolbarDefaultActions: function () {
4964            if (this.isToolbarDefaultActionsDisplayed()) {
4965                this.getToolbarActionsElement().style.display = 'none';
4966            }
4967        },
4968
4969        showToolbarDefaultActions: function () {
4970            this.hideExtensionForms();
4971
4972            if (!this.isToolbarDefaultActionsDisplayed()) {
4973                this.getToolbarActionsElement().style.display = 'block';
4974            }
4975
4976            // Using setTimeout + options.delay because:
4977            // We will actually be displaying the toolbar, which should be controlled by options.delay
4978            this.delayShowTimeout = this.base.delay(function () {
4979                this.showToolbar();
4980            }.bind(this));
4981        },
4982
4983        hideExtensionForms: function () {
4984            // Hide all extension forms
4985            this.forEachExtension(function (extension) {
4986                if (extension.hasForm && extension.isDisplayed()) {
4987                    extension.hideForm();
4988                }
4989            });
4990        },
4991
4992        // Responding to changes in user selection
4993
4994        // Checks for existance of multiple block elements in the current selection
4995        multipleBlockElementsSelected: function () {
4996            var regexEmptyHTMLTags = /<[^\/>][^>]*><\/[^>]+>/gim, // http://stackoverflow.com/questions/3129738/remove-empty-tags-using-regex
4997                regexBlockElements = new RegExp('<(' + MediumEditor.util.blockContainerElementNames.join('|') + ')[^>]*>', 'g'),
4998                selectionHTML = MediumEditor.selection.getSelectionHtml(this.document).replace(regexEmptyHTMLTags, ''), // Filter out empty blocks from selection
4999                hasMultiParagraphs = selectionHTML.match(regexBlockElements); // Find how many block elements are within the html
5000
5001            return !!hasMultiParagraphs && hasMultiParagraphs.length > 1;
5002        },
5003
5004        modifySelection: function () {
5005            var selection = this.window.getSelection(),
5006                selectionRange = selection.getRangeAt(0);
5007
5008            /*
5009            * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
5010            * will be at the very end of an element.  In other browsers, the selectionRange start
5011            * would instead be at the very beginning of an element that actually has content.
5012            * example:
5013            *   <span>foo</span><span>bar</span>
5014            *
5015            * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
5016            * of the 'bar' span.  However, there are cases where firefox will have the selectionRange start
5017            * at the end of the 'foo' span.  The contenteditable behavior will be ok, but if there are any
5018            * properties on the 'bar' span, they won't be reflected accurately in the toolbar
5019            * (ie 'Bold' button wouldn't be active)
5020            *
5021            * So, for cases where the selectionRange start is at the end of an element/node, find the next
5022            * adjacent text node that actually has content in it, and move the selectionRange start there.
5023            */
5024            if (this.standardizeSelectionStart &&
5025                    selectionRange.startContainer.nodeValue &&
5026                    (selectionRange.startOffset === selectionRange.startContainer.nodeValue.length)) {
5027                var adjacentNode = MediumEditor.util.findAdjacentTextNodeWithContent(MediumEditor.selection.getSelectionElement(this.window), selectionRange.startContainer, this.document);
5028                if (adjacentNode) {
5029                    var offset = 0;
5030                    while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
5031                        offset = offset + 1;
5032                    }
5033                    selectionRange = MediumEditor.selection.select(this.document, adjacentNode, offset,
5034                        selectionRange.endContainer, selectionRange.endOffset);
5035                }
5036            }
5037        },
5038
5039        checkState: function () {
5040            if (this.base.preventSelectionUpdates) {
5041                return;
5042            }
5043
5044            // If no editable has focus OR selection is inside contenteditable = false
5045            // hide toolbar
5046            if (!this.base.getFocusedElement() ||
5047                    MediumEditor.selection.selectionInContentEditableFalse(this.window)) {
5048                return this.hideToolbar();
5049            }
5050
5051            // If there's no selection element, selection element doesn't belong to this editor
5052            // or toolbar is disabled for this selection element
5053            // hide toolbar
5054            var selectionElement = MediumEditor.selection.getSelectionElement(this.window);
5055            if (!selectionElement ||
5056                    this.getEditorElements().indexOf(selectionElement) === -1 ||
5057                    selectionElement.getAttribute('data-disable-toolbar')) {
5058                return this.hideToolbar();
5059            }
5060
5061            // Now we know there's a focused editable with a selection
5062
5063            // If the updateOnEmptySelection option is true, show the toolbar
5064            if (this.updateOnEmptySelection && this.static) {
5065                return this.showAndUpdateToolbar();
5066            }
5067
5068            // If we don't have a 'valid' selection -> hide toolbar
5069            if (this.window.getSelection().toString().trim() === '' ||
5070                (this.allowMultiParagraphSelection === false && this.multipleBlockElementsSelected())) {
5071                return this.hideToolbar();
5072            }
5073
5074            this.showAndUpdateToolbar();
5075        },
5076
5077        // Updating the toolbar
5078
5079        showAndUpdateToolbar: function () {
5080            this.modifySelection();
5081            this.setToolbarButtonStates();
5082            this.trigger('positionToolbar', {}, this.base.getFocusedElement());
5083            this.showToolbarDefaultActions();
5084            this.setToolbarPosition();
5085        },
5086
5087        setToolbarButtonStates: function () {
5088            this.forEachExtension(function (extension) {
5089                if (typeof extension.isActive === 'function' &&
5090                    typeof extension.setInactive === 'function') {
5091                    extension.setInactive();
5092                }
5093            });
5094
5095            this.checkActiveButtons();
5096        },
5097
5098        checkActiveButtons: function () {
5099            var manualStateChecks = [],
5100                queryState = null,
5101                selectionRange = MediumEditor.selection.getSelectionRange(this.document),
5102                parentNode,
5103                updateExtensionState = function (extension) {
5104                    if (typeof extension.checkState === 'function') {
5105                        extension.checkState(parentNode);
5106                    } else if (typeof extension.isActive === 'function' &&
5107                               typeof extension.isAlreadyApplied === 'function' &&
5108                               typeof extension.setActive === 'function') {
5109                        if (!extension.isActive() && extension.isAlreadyApplied(parentNode)) {
5110                            extension.setActive();
5111                        }
5112                    }
5113                };
5114
5115            if (!selectionRange) {
5116                return;
5117            }
5118
5119            // Loop through all extensions
5120            this.forEachExtension(function (extension) {
5121                // For those extensions where we can use document.queryCommandState(), do so
5122                if (typeof extension.queryCommandState === 'function') {
5123                    queryState = extension.queryCommandState();
5124                    // If queryCommandState returns a valid value, we can trust the browser
5125                    // and don't need to do our manual checks
5126                    if (queryState !== null) {
5127                        if (queryState && typeof extension.setActive === 'function') {
5128                            extension.setActive();
5129                        }
5130                        return;
5131                    }
5132                }
5133                // We can't use queryCommandState for this extension, so add to manualStateChecks
5134                manualStateChecks.push(extension);
5135            });
5136
5137            parentNode = MediumEditor.selection.getSelectedParentElement(selectionRange);
5138
5139            // Make sure the selection parent isn't outside of the contenteditable
5140            if (!this.getEditorElements().some(function (element) {
5141                    return MediumEditor.util.isDescendant(element, parentNode, true);
5142                })) {
5143                return;
5144            }
5145
5146            // Climb up the DOM and do manual checks for whether a certain extension is currently enabled for this node
5147            while (parentNode) {
5148                manualStateChecks.forEach(updateExtensionState);
5149
5150                // we can abort the search upwards if we leave the contentEditable element
5151                if (MediumEditor.util.isMediumEditorElement(parentNode)) {
5152                    break;
5153                }
5154                parentNode = parentNode.parentNode;
5155            }
5156        },
5157
5158        // Positioning toolbar
5159
5160        positionToolbarIfShown: function () {
5161            if (this.isDisplayed()) {
5162                this.setToolbarPosition();
5163            }
5164        },
5165
5166        setToolbarPosition: function () {
5167            var container = this.base.getFocusedElement(),
5168                selection = this.window.getSelection(),
5169                anchorPreview;
5170
5171            // If there isn't a valid selection, bail
5172            if (!container) {
5173                return this;
5174            }
5175
5176            if (this.static && !this.relativeContainer) {
5177                this.showToolbar();
5178                this.positionStaticToolbar(container);
5179            } else if (!selection.isCollapsed) {
5180                this.showToolbar();
5181
5182                // we don't need any absolute positioning if relativeContainer is set
5183                if (!this.relativeContainer) {
5184                    this.positionToolbar(selection);
5185                }
5186            }
5187
5188            anchorPreview = this.base.getExtensionByName('anchor-preview');
5189
5190            if (anchorPreview && typeof anchorPreview.hidePreview === 'function') {
5191                anchorPreview.hidePreview();
5192            }
5193        },
5194
5195        positionStaticToolbar: function (container) {
5196            // position the toolbar at left 0, so we can get the real width of the toolbar
5197            this.getToolbarElement().style.left = '0';
5198
5199            // document.documentElement for IE 9
5200            var scrollTop = (this.document.documentElement && this.document.documentElement.scrollTop) || this.document.body.scrollTop,
5201                windowWidth = this.window.innerWidth,
5202                toolbarElement = this.getToolbarElement(),
5203                containerRect = container.getBoundingClientRect(),
5204                containerTop = containerRect.top + scrollTop,
5205                containerCenter = (containerRect.left + (containerRect.width / 2)),
5206                toolbarHeight = toolbarElement.offsetHeight,
5207                toolbarWidth = toolbarElement.offsetWidth,
5208                halfOffsetWidth = toolbarWidth / 2,
5209                targetLeft;
5210
5211            if (this.sticky) {
5212                // If it's beyond the height of the editor, position it at the bottom of the editor
5213                if (scrollTop > (containerTop + container.offsetHeight - toolbarHeight)) {
5214                    toolbarElement.style.top = (containerTop + container.offsetHeight - toolbarHeight) + 'px';
5215                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');
5216
5217                // Stick the toolbar to the top of the window
5218                } else if (scrollTop > (containerTop - toolbarHeight)) {
5219                    toolbarElement.classList.add('medium-editor-sticky-toolbar');
5220                    toolbarElement.style.top = '0px';
5221
5222                // Normal static toolbar position
5223                } else {
5224                    toolbarElement.classList.remove('medium-editor-sticky-toolbar');
5225                    toolbarElement.style.top = containerTop - toolbarHeight + 'px';
5226                }
5227            } else {
5228                toolbarElement.style.top = containerTop - toolbarHeight + 'px';
5229            }
5230
5231            switch (this.align) {
5232                case 'left':
5233                    targetLeft = containerRect.left;
5234                    break;
5235
5236                case 'right':
5237                    targetLeft = containerRect.right - toolbarWidth;
5238                    break;
5239
5240                case 'center':
5241                    targetLeft = containerCenter - halfOffsetWidth;
5242                    break;
5243            }
5244
5245            if (targetLeft < 0) {
5246                targetLeft = 0;
5247            } else if ((targetLeft + toolbarWidth) > windowWidth) {
5248                targetLeft = (windowWidth - Math.ceil(toolbarWidth) - 1);
5249            }
5250
5251            toolbarElement.style.left = targetLeft + 'px';
5252        },
5253
5254        positionToolbar: function (selection) {
5255            // position the toolbar at left 0, so we can get the real width of the toolbar
5256            this.getToolbarElement().style.left = '0';
5257
5258            var windowWidth = this.window.innerWidth,
5259                range = selection.getRangeAt(0),
5260                boundary = range.getBoundingClientRect(),
5261                middleBoundary = (boundary.left + boundary.right) / 2,
5262                toolbarElement = this.getToolbarElement(),
5263                toolbarHeight = toolbarElement.offsetHeight,
5264                toolbarWidth = toolbarElement.offsetWidth,
5265                halfOffsetWidth = toolbarWidth / 2,
5266                buttonHeight = 50,
5267                defaultLeft = this.diffLeft - halfOffsetWidth;
5268
5269            if (boundary.top < buttonHeight) {
5270                toolbarElement.classList.add('medium-toolbar-arrow-over');
5271                toolbarElement.classList.remove('medium-toolbar-arrow-under');
5272                toolbarElement.style.top = buttonHeight + boundary.bottom - this.diffTop + this.window.pageYOffset - toolbarHeight + 'px';
5273            } else {
5274                toolbarElement.classList.add('medium-toolbar-arrow-under');
5275                toolbarElement.classList.remove('medium-toolbar-arrow-over');
5276                toolbarElement.style.top = boundary.top + this.diffTop + this.window.pageYOffset - toolbarHeight + 'px';
5277            }
5278
5279            if (middleBoundary < halfOffsetWidth) {
5280                toolbarElement.style.left = defaultLeft + halfOffsetWidth + 'px';
5281            } else if ((windowWidth - middleBoundary) < halfOffsetWidth) {
5282                toolbarElement.style.left = windowWidth + defaultLeft - halfOffsetWidth + 'px';
5283            } else {
5284                toolbarElement.style.left = defaultLeft + middleBoundary + 'px';
5285            }
5286        }
5287    });
5288
5289    MediumEditor.extensions.toolbar = Toolbar;
5290}());
5291
5292(function () {
5293    'use strict';
5294
5295    var ImageDragging = MediumEditor.Extension.extend({
5296        init: function () {
5297            MediumEditor.Extension.prototype.init.apply(this, arguments);
5298
5299            this.subscribe('editableDrag', this.handleDrag.bind(this));
5300            this.subscribe('editableDrop', this.handleDrop.bind(this));
5301        },
5302
5303        handleDrag: function (event) {
5304            var className = 'medium-editor-dragover';
5305            event.preventDefault();
5306            event.dataTransfer.dropEffect = 'copy';
5307
5308            if (event.type === 'dragover') {
5309                event.target.classList.add(className);
5310            } else if (event.type === 'dragleave') {
5311                event.target.classList.remove(className);
5312            }
5313        },
5314
5315        handleDrop: function (event) {
5316            var className = 'medium-editor-dragover',
5317                files;
5318            event.preventDefault();
5319            event.stopPropagation();
5320
5321            // IE9 does not support the File API, so prevent file from opening in a new window
5322            // but also don't try to actually get the file
5323            if (event.dataTransfer.files) {
5324                files = Array.prototype.slice.call(event.dataTransfer.files, 0);
5325                files.some(function (file) {
5326                    if (file.type.match('image')) {
5327                        var fileReader, id;
5328                        fileReader = new FileReader();
vendor: 4,929 bytes, lines 5329-5432
5329                        fileReader.readAsDataURL(file);
5330
5331                        id = 'medium-img-' + (+new Date());
5332                        MediumEditor.util.insertHTMLCommand(this.document, '<img class="medium-editor-image-loading" id="' + id + '" />');
5333
5334                        fileReader.onload = function () {
5335                            var img = this.document.getElementById(id);
5336                            if (img) {
5337                                img.removeAttribute('id');
5338                                img.removeAttribute('class');
5339                                img.src = fileReader.result;
5340                            }
5341                        }.bind(this);
5342                    }
5343                }.bind(this));
5344            }
5345            event.target.classList.remove(className);
5346        }
5347    });
5348
5349    MediumEditor.extensions.imageDragging = ImageDragging;
5350}());
5351
5352(function () {
5353    'use strict';
5354
5355    // Event handlers that shouldn't be exposed externally
5356
5357    function handleDisabledEnterKeydown(event, element) {
5358        if (this.options.disableReturn || element.getAttribute('data-disable-return')) {
5359            event.preventDefault();
5360        } else if (this.options.disableDoubleReturn || element.getAttribute('data-disable-double-return')) {
5361            var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument);
5362
5363            // if current text selection is empty OR previous sibling text is empty
5364            if ((node && node.textContent.trim() === '') ||
5365                (node.previousElementSibling && node.previousElementSibling.textContent.trim() === '')) {
5366                event.preventDefault();
5367            }
5368        }
5369    }
5370
5371    function handleTabKeydown(event) {
5372        // Override tab only for pre nodes
5373        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
5374            tag = node && node.nodeName.toLowerCase();
5375
5376        if (tag === 'pre') {
5377            event.preventDefault();
5378            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, '    ');
5379        }
5380
5381        // Tab to indent list structures!
5382        if (MediumEditor.util.isListItem(node)) {
5383            event.preventDefault();
5384
5385            // If Shift is down, outdent, otherwise indent
5386            if (event.shiftKey) {
5387                this.options.ownerDocument.execCommand('outdent', false, null);
5388            } else {
5389                this.options.ownerDocument.execCommand('indent', false, null);
5390            }
5391        }
5392    }
5393
5394    function handleBlockDeleteKeydowns(event) {
5395        var p, node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
5396            tagName = node.nodeName.toLowerCase(),
5397            isEmpty = /^(\s+|<br\/?>)?$/i,
5398            isHeader = /h\d/i;
5399
5400        if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.BACKSPACE, MediumEditor.util.keyCode.ENTER]) &&
5401                // has a preceeding sibling
5402                node.previousElementSibling &&
5403                // in a header
5404                isHeader.test(tagName) &&
5405                // at the very end of the block
5406                MediumEditor.selection.getCaretOffsets(node).left === 0) {
5407            if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) && isEmpty.test(node.previousElementSibling.innerHTML)) {
5408                // backspacing the begining of a header into an empty previous element will
5409                // change the tagName of the current node to prevent one
5410                // instead delete previous node and cancel the event.
5411                node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
5412                event.preventDefault();
5413            } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER)) {
5414                // hitting return in the begining of a header will create empty header elements before the current one
5415                // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
5416                p = this.options.ownerDocument.createElement('p');
5417                p.innerHTML = '<br>';
5418                node.previousElementSibling.parentNode.insertBefore(p, node);
5419                event.preventDefault();
5420            }
5421        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.DELETE) &&
5422                    // between two sibling elements
5423                    node.nextElementSibling &&
5424                    node.previousElementSibling &&
5425                    // not in a header
5426                    !isHeader.test(tagName) &&
5427                    // in an empty tag
5428                    isEmpty.test(node.innerHTML) &&
5429                    // when the next tag *is* a header
5430                    isHeader.test(node.nextElementSibling.nodeName.toLowerCase())) {
5431            // hitting delete in an empty element preceding a header, ex:
5432            //  <p>[CURSOR]</p><h1>Header</h1>
5433            // Will cause the h1 to become a paragraph.
5434            // Instead, delete the paragraph node and move the cursor to the begining of the h1
5435
5436            // remove node and move cursor to start of header
5437            MediumEditor.selection.moveCursor(this.options.ownerDocument, node.nextElementSibling);
5438
5439            node.previousElementSibling.parentNode.removeChild(node);
5440
5441            event.preventDefault();
5442        } else if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.BACKSPACE) &&
5443                tagName === 'li' &&
5444                // hitting backspace inside an empty li
5445                isEmpty.test(node.innerHTML) &&
5446                // is first element (no preceeding siblings)
5447                !node.previousElementSibling &&
5448                // parent also does not have a sibling
5449                !node.parentElement.previousElementSibling &&
5450                // is not the only li in a list
5451                node.nextElementSibling &&
5452                node.nextElementSibling.nodeName.toLowerCase() === 'li') {
5453            // backspacing in an empty first list element in the first list (with more elements) ex:
5454            //  <ul><li>[CURSOR]</li><li>List Item 2</li></ul>
5455            // will remove the first <li> but add some extra element before (varies based on browser)
5456            // Instead, this will:
5457            // 1) remove the list element
5458            // 2) create a paragraph before the list
5459            // 3) move the cursor into the paragraph
5460
5461            // create a paragraph before the list
5462            p = this.options.ownerDocument.createElement('p');
5463            p.innerHTML = '<br>';
5464            node.parentElement.parentElement.insertBefore(p, node.parentElement);
5465
5466            // move the cursor into the new paragraph
5467            MediumEditor.selection.moveCursor(this.options.ownerDocument, p);
5468
5469            // remove the list element
5470            node.parentElement.removeChild(node);
5471
5472            event.preventDefault();
5473        }
5474    }
5475
5476    function handleKeyup(event) {
5477        var node = MediumEditor.selection.getSelectionStart(this.options.ownerDocument),
5478            tagName;
5479
5480        if (!node) {
5481            return;
5482        }
5483
5484        if (MediumEditor.util.isMediumEditorElement(node) && node.children.length === 0) {
5485            this.options.ownerDocument.execCommand('formatBlock', false, 'p');
5486        }
5487
5488        if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) && !MediumEditor.util.isListItem(node)) {
5489            tagName = node.nodeName.toLowerCase();
5490            // For anchor tags, unlink
5491            if (tagName === 'a') {
5492                this.options.ownerDocument.execCommand('unlink', false, null);
5493            } else if (!event.shiftKey && !event.ctrlKey) {
5494                // only format block if this is not a header tag
5495                if (!/h\d/.test(tagName)) {
5496                    this.options.ownerDocument.execCommand('formatBlock', false, 'p');
5497                }
5498            }
5499        }
5500    }
5501
5502    // Internal helper methods which shouldn't be exposed externally
5503
5504    function addToEditors(win) {
5505        if (!win._mediumEditors) {
5506            // To avoid breaking users who are assuming that the unique id on
5507            // medium-editor elements will start at 1, inserting a 'null' in the
5508            // array so the unique-id can always map to the index of the editor instance
5509            win._mediumEditors = [null];
5510        }
5511
5512        // If this already has a unique id, re-use it
5513        if (!this.id) {
5514            this.id = win._mediumEditors.length;
5515        }
5516
5517        win._mediumEditors[this.id] = this;
5518    }
5519
5520    function removeFromEditors(win) {
5521        if (!win._mediumEditors || !win._mediumEditors[this.id]) {
5522            return;
5523        }
5524
5525        /* Setting the instance to null in the array instead of deleting it allows:
5526         * 1) Each instance to preserve its own unique-id, even after being destroyed
5527         *    and initialized again
5528         * 2) The unique-id to always correspond to an index in the array of medium-editor
5529         *    instances. Thus, we will be able to look at a contenteditable, and determine
5530         *    which instance it belongs to, by indexing into the global array.
5531         */
5532        win._mediumEditors[this.id] = null;
5533    }
5534
5535    function createElementsArray(selector) {
5536        if (!selector) {
5537            selector = [];
5538        }
5539        // If string, use as query selector
5540        if (typeof selector === 'string') {
5541            selector = this.options.ownerDocument.querySelectorAll(selector);
5542        }
5543        // If element, put into array
5544        if (MediumEditor.util.isElement(selector)) {
5545            selector = [selector];
5546        }
5547        // Convert NodeList (or other array like object) into an array
5548        var elements = Array.prototype.slice.apply(selector);
5549
5550        // Loop through elements and convert textarea's into divs
5551        this.elements = [];
5552        elements.forEach(function (element, index) {
5553            if (element.nodeName.toLowerCase() === 'textarea') {
5554                this.elements.push(createContentEditable.call(this, element, index));
5555            } else {
5556                this.elements.push(element);
5557            }
5558        }, this);
5559    }
5560
5561    function setExtensionDefaults(extension, defaults) {
5562        Object.keys(defaults).forEach(function (prop) {
5563            if (extension[prop] === undefined) {
5564                extension[prop] = defaults[prop];
5565            }
5566        });
5567        return extension;
5568    }
5569
5570    function initExtension(extension, name, instance) {
5571        var extensionDefaults = {
5572            'window': instance.options.contentWindow,
5573            'document': instance.options.ownerDocument,
5574            'base': instance
5575        };
5576
5577        // Add default options into the extension
5578        extension = setExtensionDefaults(extension, extensionDefaults);
5579
5580        // Call init on the extension
5581        if (typeof extension.init === 'function') {
5582            extension.init();
5583        }
5584
5585        // Set extension name (if not already set)
5586        if (!extension.name) {
5587            extension.name = name;
5588        }
5589        return extension;
5590    }
5591
5592    function isToolbarEnabled() {
5593        // If any of the elements don't have the toolbar disabled
5594        // We need a toolbar
5595        if (this.elements.every(function (element) {
5596                return !!element.getAttribute('data-disable-toolbar');
5597            })) {
5598            return false;
5599        }
5600
5601        return this.options.toolbar !== false;
5602    }
5603
5604    function isAnchorPreviewEnabled() {
5605        // If toolbar is disabled, don't add
5606        if (!isToolbarEnabled.call(this)) {
5607            return false;
5608        }
5609
5610        return this.options.anchorPreview !== false;
5611    }
5612
5613    function isPlaceholderEnabled() {
5614        return this.options.placeholder !== false;
5615    }
5616
5617    function isAutoLinkEnabled() {
5618        return this.options.autoLink !== false;
5619    }
5620
5621    function isImageDraggingEnabled() {
5622        return this.options.imageDragging !== false;
5623    }
5624
5625    function isKeyboardCommandsEnabled() {
5626        return this.options.keyboardCommands !== false;
5627    }
5628
5629    function shouldUseFileDraggingExtension() {
5630        // Since the file-dragging extension replaces the image-dragging extension,
5631        // we need to check if the user passed an overrided image-dragging extension.
5632        // If they have, to avoid breaking users, we won't use file-dragging extension.
5633        return !this.options.extensions['imageDragging'];
5634    }
5635
5636    function createContentEditable(textarea, id) {
5637        var div = this.options.ownerDocument.createElement('div'),
5638            now = Date.now(),
5639            uniqueId = 'medium-editor-' + now + '-' + id,
5640            atts = textarea.attributes;
5641
5642        // Some browsers can move pretty fast, since we're using a timestamp
5643        // to make a unique-id, ensure that the id is actually unique on the page
5644        while (this.options.ownerDocument.getElementById(uniqueId)) {
5645            now++;
5646            uniqueId = 'medium-editor-' + now + '-' + id;
5647        }
5648
5649        div.className = textarea.className;
5650        div.id = uniqueId;
5651        div.innerHTML = textarea.value;
5652
5653        textarea.setAttribute('medium-editor-textarea-id', uniqueId);
5654
5655        // re-create all attributes from the textearea to the new created div
5656        for (var i = 0, n = atts.length; i < n; i++) {
5657            // do not re-create existing attributes
5658            if (!div.hasAttribute(atts[i].nodeName)) {
5659                div.setAttribute(atts[i].nodeName, atts[i].nodeValue);
5660            }
5661        }
5662
5663        textarea.classList.add('medium-editor-hidden');
5664        textarea.parentNode.insertBefore(
5665            div,
5666            textarea
5667        );
5668
5669        return div;
5670    }
5671
5672    function initElements() {
5673        this.elements.forEach(function (element, index) {
5674            if (!this.options.disableEditing && !element.getAttribute('data-disable-editing')) {
5675                element.setAttribute('contentEditable', true);
5676                element.setAttribute('spellcheck', this.options.spellcheck);
5677            }
5678            element.setAttribute('data-medium-editor-element', true);
5679            element.setAttribute('role', 'textbox');
5680            element.setAttribute('aria-multiline', true);
5681            element.setAttribute('medium-editor-index', index);
5682
5683            if (element.hasAttribute('medium-editor-textarea-id')) {
5684                this.on(element, 'input', function (event) {
5685                    var target = event.target,
5686                        textarea = target.parentNode.querySelector('textarea[medium-editor-textarea-id="' + target.getAttribute('medium-editor-textarea-id') + '"]');
5687                    if (textarea) {
5688                        textarea.value = this.serialize()[target.id].value;
5689                    }
5690                }.bind(this));
5691            }
5692        }, this);
5693    }
5694
5695    function attachHandlers() {
5696        var i;
5697
5698        // attach to tabs
5699        this.subscribe('editableKeydownTab', handleTabKeydown.bind(this));
5700
5701        // Bind keys which can create or destroy a block element: backspace, delete, return
5702        this.subscribe('editableKeydownDelete', handleBlockDeleteKeydowns.bind(this));
5703        this.subscribe('editableKeydownEnter', handleBlockDeleteKeydowns.bind(this));
5704
5705        // disabling return or double return
5706        if (this.options.disableReturn || this.options.disableDoubleReturn) {
5707            this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
5708        } else {
5709            for (i = 0; i < this.elements.length; i += 1) {
5710                if (this.elements[i].getAttribute('data-disable-return') || this.elements[i].getAttribute('data-disable-double-return')) {
5711                    this.subscribe('editableKeydownEnter', handleDisabledEnterKeydown.bind(this));
5712                    break;
5713                }
5714            }
5715        }
5716
5717        // if we're not disabling return, add a handler to help handle cleanup
5718        // for certain cases when enter is pressed
5719        if (!this.options.disableReturn) {
5720            this.elements.forEach(function (element) {
5721                if (!element.getAttribute('data-disable-return')) {
5722                    this.on(element, 'keyup', handleKeyup.bind(this));
5723                }
5724            }, this);
5725        }
5726    }
5727
5728    function initExtensions() {
5729
5730        this.extensions = [];
5731
5732        // Passed in extensions
vendor: 4,378 bytes, lines 5733-5834
5733        Object.keys(this.options.extensions).forEach(function (name) {
5734            // Always save the toolbar extension for last
5735            if (name !== 'toolbar' && this.options.extensions[name]) {
5736                this.extensions.push(initExtension(this.options.extensions[name], name, this));
5737            }
5738        }, this);
5739
5740        // 4 Cases for imageDragging + fileDragging extensons:
5741        //
5742        // 1. ImageDragging ON + No Custom Image Dragging Extension:
5743        //    * Use fileDragging extension (default options)
5744        // 2. ImageDragging OFF + No Custom Image Dragging Extension:
5745        //    * Use fileDragging extension w/ images turned off
5746        // 3. ImageDragging ON + Custom Image Dragging Extension:
5747        //    * Don't use fileDragging (could interfere with custom image dragging extension)
5748        // 4. ImageDragging OFF + Custom Image Dragging:
5749        //    * Don't use fileDragging (could interfere with custom image dragging extension)
5750        if (shouldUseFileDraggingExtension.call(this)) {
5751            var opts = this.options.fileDragging;
5752            if (!opts) {
5753                opts = {};
5754
5755                // Image is in the 'allowedTypes' list by default.
5756                // If imageDragging is off override the 'allowedTypes' list with an empty one
5757                if (!isImageDraggingEnabled.call(this)) {
5758                    opts.allowedTypes = [];
5759                }
5760            }
5761            this.addBuiltInExtension('fileDragging', opts);
5762        }
5763
5764        // Built-in extensions
5765        var builtIns = {
5766            paste: true,
5767            'anchor-preview': isAnchorPreviewEnabled.call(this),
5768            autoLink: isAutoLinkEnabled.call(this),
5769            keyboardCommands: isKeyboardCommandsEnabled.call(this),
5770            placeholder: isPlaceholderEnabled.call(this)
5771        };
5772        Object.keys(builtIns).forEach(function (name) {
5773            if (builtIns[name]) {
5774                this.addBuiltInExtension(name);
5775            }
5776        }, this);
5777
5778        // Users can pass in a custom toolbar extension
5779        // so check for that first and if it's not present
5780        // just create the default toolbar
5781        var toolbarExtension = this.options.extensions['toolbar'];
5782        if (!toolbarExtension && isToolbarEnabled.call(this)) {
5783            // Backwards compatability
5784            var toolbarOptions = MediumEditor.util.extend({}, this.options.toolbar, {
5785                allowMultiParagraphSelection: this.options.allowMultiParagraphSelection // deprecated
5786            });
5787            toolbarExtension = new MediumEditor.extensions.toolbar(toolbarOptions);
5788        }
5789
5790        // If the toolbar is not disabled, so we actually have an extension
5791        // initialize it and add it to the extensions array
5792        if (toolbarExtension) {
5793            this.extensions.push(initExtension(toolbarExtension, 'toolbar', this));
5794        }
5795    }
5796
5797    function mergeOptions(defaults, options) {
5798        var deprecatedProperties = [
5799            ['allowMultiParagraphSelection', 'toolbar.allowMultiParagraphSelection']
5800        ];
5801        // warn about using deprecated properties
5802        if (options) {
5803            deprecatedProperties.forEach(function (pair) {
5804                if (options.hasOwnProperty(pair[0]) && options[pair[0]] !== undefined) {
5805                    MediumEditor.util.deprecated(pair[0], pair[1], 'v6.0.0');
5806                }
5807            });
5808        }
5809
5810        return MediumEditor.util.defaults({}, options, defaults);
5811    }
5812
5813    function execActionInternal(action, opts) {
5814        /*jslint regexp: true*/
5815        var appendAction = /^append-(.+)$/gi,
5816            justifyAction = /justify([A-Za-z]*)$/g, /* Detecting if is justifyCenter|Right|Left */
5817            match;
5818        /*jslint regexp: false*/
5819
5820        // Actions starting with 'append-' should attempt to format a block of text ('formatBlock') using a specific
5821        // type of block element (ie append-blockquote, append-h1, append-pre, etc.)
5822        match = appendAction.exec(action);
5823        if (match) {
5824            return MediumEditor.util.execFormatBlock(this.options.ownerDocument, match[1]);
5825        }
5826
5827        if (action === 'fontSize') {
5828            return this.options.ownerDocument.execCommand('fontSize', false, opts.size);
5829        }
5830
5831        if (action === 'createLink') {
5832            return this.createLink(opts);
5833        }
5834
5835        if (action === 'image') {
5836            return this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
5837        }
5838
5839        /* Issue: https://github.com/yabwe/medium-editor/issues/595
5840         * If the action is to justify the text */
5841        if (justifyAction.exec(action)) {
5842            var result = this.options.ownerDocument.execCommand(action, false, null),
5843                parentNode = MediumEditor.selection.getSelectedParentElement(MediumEditor.selection.getSelectionRange(this.options.ownerDocument));
5844            if (parentNode) {
5845                cleanupJustifyDivFragments.call(this, MediumEditor.util.getTopBlockContainer(parentNode));
5846            }
5847
5848            return result;
5849        }
5850
5851        return this.options.ownerDocument.execCommand(action, false, null);
5852    }
5853
5854    /* If we've just justified text within a container block
5855     * Chrome may have removed <br> elements and instead wrapped lines in <div> elements
5856     * with a text-align property.  If so, we want to fix this
5857     */
5858    function cleanupJustifyDivFragments(blockContainer) {
5859        if (!blockContainer) {
5860            return;
5861        }
5862
5863        var textAlign,
5864            childDivs = Array.prototype.slice.call(blockContainer.childNodes).filter(function (element) {
5865                var isDiv = element.nodeName.toLowerCase() === 'div';
5866                if (isDiv && !textAlign) {
5867                    textAlign = element.style.textAlign;
5868                }
5869                return isDiv;
5870            });
5871
5872        /* If we found child <div> elements with text-align style attributes
5873         * we should fix this by:
5874         *
5875         * 1) Unwrapping each <div> which has a text-align style
5876         * 2) Insert a <br> element after each set of 'unwrapped' div children
5877         * 3) Set the text-align style of the parent block element
5878         */
5879        if (childDivs.length) {
5880            // Since we're mucking with the HTML, preserve selection
5881            this.saveSelection();
5882            childDivs.forEach(function (div) {
5883                if (div.style.textAlign === textAlign) {
5884                    var lastChild = div.lastChild;
5885                    if (lastChild) {
5886                        // Instead of a div, extract the child elements and add a <br>
5887                        MediumEditor.util.unwrap(div, this.options.ownerDocument);
5888                        var br = this.options.ownerDocument.createElement('BR');
5889                        lastChild.parentNode.insertBefore(br, lastChild.nextSibling);
5890                    }
5891                }
5892            }, this);
5893            blockContainer.style.textAlign = textAlign;
5894            // We're done, so restore selection
5895            this.restoreSelection();
5896        }
5897    }
5898
5899    MediumEditor.prototype = {
5900        // NOT DOCUMENTED - exposed for backwards compatability
5901        init: function (elements, options) {
5902            this.options = mergeOptions.call(this, this.defaults, options);
5903            this.origElements = elements;
5904
5905            if (!this.options.elementsContainer) {
5906                this.options.elementsContainer = this.options.ownerDocument.body;
5907            }
5908
5909            return this.setup();
5910        },
5911
5912        setup: function () {
5913            if (this.isActive) {
5914                return;
5915            }
5916
5917            createElementsArray.call(this, this.origElements);
5918
5919            if (this.elements.length === 0) {
5920                return;
5921            }
5922
5923            this.isActive = true;
5924            addToEditors.call(this, this.options.contentWindow);
5925
5926            this.events = new MediumEditor.Events(this);
5927
5928            // Call initialization helpers
5929            initElements.call(this);
5930            initExtensions.call(this);
5931            attachHandlers.call(this);
5932        },
5933
5934        destroy: function () {
5935            if (!this.isActive) {
5936                return;
5937            }
5938
5939            this.isActive = false;
5940
5941            this.extensions.forEach(function (extension) {
5942                if (typeof extension.destroy === 'function') {
5943                    extension.destroy();
5944                }
5945            }, this);
5946
5947            this.events.destroy();
5948
vendor: 22,249 bytes, lines 5949-6445
5949            this.elements.forEach(function (element) {
5950                // Reset elements content, fix for issue where after editor destroyed the red underlines on spelling errors are left
5951                if (this.options.spellcheck) {
5952                    element.innerHTML = element.innerHTML;
5953                }
5954
5955                // cleanup extra added attributes
5956                element.removeAttribute('contentEditable');
5957                element.removeAttribute('spellcheck');
5958                element.removeAttribute('data-medium-editor-element');
5959                element.removeAttribute('role');
5960                element.removeAttribute('aria-multiline');
5961                element.removeAttribute('medium-editor-index');
5962
5963                // Remove any elements created for textareas
5964                if (element.hasAttribute('medium-editor-textarea-id')) {
5965                    var textarea = element.parentNode.querySelector('textarea[medium-editor-textarea-id="' + element.getAttribute('medium-editor-textarea-id') + '"]');
5966                    if (textarea) {
5967                        // Un-hide the textarea
5968                        textarea.classList.remove('medium-editor-hidden');
5969                    }
5970                    if (element.parentNode) {
5971                        element.parentNode.removeChild(element);
5972                    }
5973                }
5974            }, this);
5975            this.elements = [];
5976
5977            removeFromEditors.call(this, this.options.contentWindow);
5978        },
5979
5980        on: function (target, event, listener, useCapture) {
5981            this.events.attachDOMEvent(target, event, listener, useCapture);
5982        },
5983
5984        off: function (target, event, listener, useCapture) {
5985            this.events.detachDOMEvent(target, event, listener, useCapture);
5986        },
5987
5988        subscribe: function (event, listener) {
5989            this.events.attachCustomEvent(event, listener);
5990        },
5991
5992        unsubscribe: function (event, listener) {
5993            this.events.detachCustomEvent(event, listener);
5994        },
5995
5996        trigger: function (name, data, editable) {
5997            this.events.triggerCustomEvent(name, data, editable);
5998        },
5999
6000        delay: function (fn) {
6001            var self = this;
6002            return setTimeout(function () {
6003                if (self.isActive) {
6004                    fn();
6005                }
6006            }, this.options.delay);
6007        },
6008
6009        serialize: function () {
6010            var i,
6011                elementid,
6012                content = {};
6013            for (i = 0; i < this.elements.length; i += 1) {
6014                elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
6015                content[elementid] = {
6016                    value: this.elements[i].innerHTML.trim()
6017                };
6018            }
6019            return content;
6020        },
6021
6022        getExtensionByName: function (name) {
6023            var extension;
6024            if (this.extensions && this.extensions.length) {
6025                this.extensions.some(function (ext) {
6026                    if (ext.name === name) {
6027                        extension = ext;
6028                        return true;
6029                    }
6030                    return false;
6031                });
6032            }
6033            return extension;
6034        },
6035
6036        /**
6037         * NOT DOCUMENTED - exposed as a helper for other extensions to use
6038         */
6039        addBuiltInExtension: function (name, opts) {
6040            var extension = this.getExtensionByName(name),
6041                merged;
6042            if (extension) {
6043                return extension;
6044            }
6045
6046            switch (name) {
6047                case 'anchor':
6048                    merged = MediumEditor.util.extend({}, this.options.anchor, opts);
6049                    extension = new MediumEditor.extensions.anchor(merged);
6050                    break;
6051                case 'anchor-preview':
6052                    extension = new MediumEditor.extensions.anchorPreview(this.options.anchorPreview);
6053                    break;
6054                case 'autoLink':
6055                    extension = new MediumEditor.extensions.autoLink();
6056                    break;
6057                case 'fileDragging':
6058                    extension = new MediumEditor.extensions.fileDragging(opts);
6059                    break;
6060                case 'fontsize':
6061                    extension = new MediumEditor.extensions.fontSize(opts);
6062                    break;
6063                case 'keyboardCommands':
6064                    extension = new MediumEditor.extensions.keyboardCommands(this.options.keyboardCommands);
6065                    break;
6066                case 'paste':
6067                    extension = new MediumEditor.extensions.paste(this.options.paste);
6068                    break;
6069                case 'placeholder':
6070                    extension = new MediumEditor.extensions.placeholder(this.options.placeholder);
6071                    break;
6072                default:
6073                    // All of the built-in buttons for MediumEditor are extensions
6074                    // so check to see if the extension we're creating is a built-in button
6075                    if (MediumEditor.extensions.button.isBuiltInButton(name)) {
6076                        if (opts) {
6077                            merged = MediumEditor.util.defaults({}, opts, MediumEditor.extensions.button.prototype.defaults[name]);
6078                            extension = new MediumEditor.extensions.button(merged);
6079                        } else {
6080                            extension = new MediumEditor.extensions.button(name);
6081                        }
6082                    }
6083            }
6084
6085            if (extension) {
6086                this.extensions.push(initExtension(extension, name, this));
6087            }
6088
6089            return extension;
6090        },
6091
6092        stopSelectionUpdates: function () {
6093            this.preventSelectionUpdates = true;
6094        },
6095
6096        startSelectionUpdates: function () {
6097            this.preventSelectionUpdates = false;
6098        },
6099
6100        checkSelection: function () {
6101            var toolbar = this.getExtensionByName('toolbar');
6102            if (toolbar) {
6103                toolbar.checkState();
6104            }
6105            return this;
6106        },
6107
6108        // Wrapper around document.queryCommandState for checking whether an action has already
6109        // been applied to the current selection
6110        queryCommandState: function (action) {
6111            var fullAction = /^full-(.+)$/gi,
6112                match,
6113                queryState = null;
6114
6115            // Actions starting with 'full-' need to be modified since this is a medium-editor concept
6116            match = fullAction.exec(action);
6117            if (match) {
6118                action = match[1];
6119            }
6120
6121            try {
6122                queryState = this.options.ownerDocument.queryCommandState(action);
6123            } catch (exc) {
6124                queryState = null;
6125            }
6126
6127            return queryState;
6128        },
6129
6130        execAction: function (action, opts) {
6131            /*jslint regexp: true*/
6132            var fullAction = /^full-(.+)$/gi,
6133                match,
6134                result;
6135            /*jslint regexp: false*/
6136
6137            // Actions starting with 'full-' should be applied to to the entire contents of the editable element
6138            // (ie full-bold, full-append-pre, etc.)
6139            match = fullAction.exec(action);
6140            if (match) {
6141                // Store the current selection to be restored after applying the action
6142                this.saveSelection();
6143                // Select all of the contents before calling the action
6144                this.selectAllContents();
6145                result = execActionInternal.call(this, match[1], opts);
6146                // Restore the previous selection
6147                this.restoreSelection();
6148            } else {
6149                result = execActionInternal.call(this, action, opts);
6150            }
6151
6152            // do some DOM clean-up for known browser issues after the action
6153            if (action === 'insertunorderedlist' || action === 'insertorderedlist') {
6154                MediumEditor.util.cleanListDOM(this.options.ownerDocument, this.getSelectedParentElement());
6155            }
6156
6157            this.checkSelection();
6158            return result;
6159        },
6160
6161        getSelectedParentElement: function (range) {
6162            if (range === undefined) {
6163                range = this.options.contentWindow.getSelection().getRangeAt(0);
6164            }
6165            return MediumEditor.selection.getSelectedParentElement(range);
6166        },
6167
6168        selectAllContents: function () {
6169            var currNode = MediumEditor.selection.getSelectionElement(this.options.contentWindow);
6170
6171            if (currNode) {
6172                // Move to the lowest descendant node that still selects all of the contents
6173                while (currNode.children.length === 1) {
6174                    currNode = currNode.children[0];
6175                }
6176
6177                this.selectElement(currNode);
6178            }
6179        },
6180
6181        selectElement: function (element) {
6182            MediumEditor.selection.selectNode(element, this.options.ownerDocument);
6183
6184            var selElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow);
6185            if (selElement) {
6186                this.events.focusElement(selElement);
6187            }
6188        },
6189
6190        getFocusedElement: function () {
6191            var focused;
6192            this.elements.some(function (element) {
6193                // Find the element that has focus
6194                if (!focused && element.getAttribute('data-medium-focused')) {
6195                    focused = element;
6196                }
6197
6198                // bail if we found the element that had focus
6199                return !!focused;
6200            }, this);
6201
6202            return focused;
6203        },
6204
6205        // Export the state of the selection in respect to one of this
6206        // instance of MediumEditor's elements
6207        exportSelection: function () {
6208            var selectionElement = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
6209                editableElementIndex = this.elements.indexOf(selectionElement),
6210                selectionState = null;
6211
6212            if (editableElementIndex >= 0) {
6213                selectionState = MediumEditor.selection.exportSelection(selectionElement, this.options.ownerDocument);
6214            }
6215
6216            if (selectionState !== null && editableElementIndex !== 0) {
6217                selectionState.editableElementIndex = editableElementIndex;
6218            }
6219
6220            return selectionState;
6221        },
6222
6223        saveSelection: function () {
6224            this.selectionState = this.exportSelection();
6225        },
6226
6227        // Restore a selection based on a selectionState returned by a call
6228        // to MediumEditor.exportSelection
6229        importSelection: function (selectionState, favorLaterSelectionAnchor) {
6230            if (!selectionState) {
6231                return;
6232            }
6233
6234            var editableElement = this.elements[selectionState.editableElementIndex || 0];
6235            MediumEditor.selection.importSelection(selectionState, editableElement, this.options.ownerDocument, favorLaterSelectionAnchor);
6236        },
6237
6238        restoreSelection: function () {
6239            this.importSelection(this.selectionState);
6240        },
6241
6242        createLink: function (opts) {
6243            var currentEditor = MediumEditor.selection.getSelectionElement(this.options.contentWindow),
6244                customEvent = {};
6245
6246            // Make sure the selection is within an element this editor is tracking
6247            if (this.elements.indexOf(currentEditor) === -1) {
6248                return;
6249            }
6250
6251            try {
6252                this.events.disableCustomEvent('editableInput');
6253                if (opts.url && opts.url.trim().length > 0) {
6254                    var currentSelection = this.options.contentWindow.getSelection();
6255                    if (currentSelection) {
6256                        var currRange = currentSelection.getRangeAt(0),
6257                            commonAncestorContainer = currRange.commonAncestorContainer,
6258                            exportedSelection,
6259                            startContainerParentElement,
6260                            endContainerParentElement,
6261                            textNodes;
6262
6263                        // If the selection is contained within a single text node
6264                        // and the selection starts at the beginning of the text node,
6265                        // MSIE still says the startContainer is the parent of the text node.
6266                        // If the selection is contained within a single text node, we
6267                        // want to just use the default browser 'createLink', so we need
6268                        // to account for this case and adjust the commonAncestorContainer accordingly
6269                        if (currRange.endContainer.nodeType === 3 &&
6270                            currRange.startContainer.nodeType !== 3 &&
6271                            currRange.startOffset === 0 &&
6272                            currRange.startContainer.firstChild === currRange.endContainer) {
6273                            commonAncestorContainer = currRange.endContainer;
6274                        }
6275
6276                        startContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.startContainer);
6277                        endContainerParentElement = MediumEditor.util.getClosestBlockContainer(currRange.endContainer);
6278
6279                        // If the selection is not contained within a single text node
6280                        // but the selection is contained within the same block element
6281                        // we want to make sure we create a single link, and not multiple links
6282                        // which can happen with the built in browser functionality
6283                        if (commonAncestorContainer.nodeType !== 3 && startContainerParentElement === endContainerParentElement) {
6284                            var parentElement = (startContainerParentElement || currentEditor),
6285                                fragment = this.options.ownerDocument.createDocumentFragment();
6286
6287                            // since we are going to create a link from an extracted text,
6288                            // be sure that if we are updating a link, we won't let an empty link behind (see #754)
6289                            // (Workaroung for Chrome)
6290                            this.execAction('unlink');
6291
6292                            exportedSelection = this.exportSelection();
6293                            fragment.appendChild(parentElement.cloneNode(true));
6294
6295                            if (currentEditor === parentElement) {
6296                                // We have to avoid the editor itself being wiped out when it's the only block element,
6297                                // as our reference inside this.elements gets detached from the page when insertHTML runs.
6298                                // If we just use [parentElement, 0] and [parentElement, parentElement.childNodes.length]
6299                                // as the range boundaries, this happens whenever parentElement === currentEditor.
6300                                // The tradeoff to this workaround is that a orphaned tag can sometimes be left behind at
6301                                // the end of the editor's content.
6302                                // In Gecko:
6303                                // as an empty <strong></strong> if parentElement.lastChild is a <strong> tag.
6304                                // In WebKit:
6305                                // an invented <br /> tag at the end in the same situation
6306                                MediumEditor.selection.select(
6307                                    this.options.ownerDocument,
6308                                    parentElement.firstChild,
6309                                    0,
6310                                    parentElement.lastChild,
6311                                    parentElement.lastChild.nodeType === 3 ?
6312                                    parentElement.lastChild.nodeValue.length : parentElement.lastChild.childNodes.length
6313                                );
6314                            } else {
6315                                MediumEditor.selection.select(
6316                                    this.options.ownerDocument,
6317                                    parentElement,
6318                                    0,
6319                                    parentElement,
6320                                    parentElement.childNodes.length
6321                                );
6322                            }
6323
6324                            var modifiedExportedSelection = this.exportSelection();
6325
6326                            textNodes = MediumEditor.util.findOrCreateMatchingTextNodes(
6327                                this.options.ownerDocument,
6328                                fragment,
6329                                {
6330                                    start: exportedSelection.start - modifiedExportedSelection.start,
6331                                    end: exportedSelection.end - modifiedExportedSelection.start,
6332                                    editableElementIndex: exportedSelection.editableElementIndex
6333                                }
6334                            );
6335                            // If textNodes are not present, when changing link on images
6336                            // ex: <a><img src="http://image.test.com"></a>, change fragment to currRange.startContainer
6337                            // and set textNodes array to [imageElement, imageElement]
6338                            if (textNodes.length === 0) {
6339                                fragment = this.options.ownerDocument.createDocumentFragment();
6340                                fragment.appendChild(commonAncestorContainer.cloneNode(true));
6341                                textNodes = [fragment.firstChild.firstChild, fragment.firstChild.lastChild];
6342                            }
6343
6344                            // Creates the link in the document fragment
6345                            MediumEditor.util.createLink(this.options.ownerDocument, textNodes, opts.url.trim());
6346
6347                            // Chrome trims the leading whitespaces when inserting HTML, which messes up restoring the selection.
6348                            var leadingWhitespacesCount = (fragment.firstChild.innerHTML.match(/^\s+/) || [''])[0].length;
6349
6350                            // Now move the created link back into the original document in a way to preserve undo/redo history
6351                            MediumEditor.util.insertHTMLCommand(this.options.ownerDocument, fragment.firstChild.innerHTML.replace(/^\s+/, ''));
6352                            exportedSelection.start -= leadingWhitespacesCount;
6353                            exportedSelection.end -= leadingWhitespacesCount;
6354
6355                            this.importSelection(exportedSelection);
6356                        } else {
6357                            this.options.ownerDocument.execCommand('createLink', false, opts.url);
6358                        }
6359
6360                        if (this.options.targetBlank || opts.target === '_blank') {
6361                            MediumEditor.util.setTargetBlank(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.url);
6362                        }
6363
6364                        if (opts.buttonClass) {
6365                            MediumEditor.util.addClassToAnchors(MediumEditor.selection.getSelectionStart(this.options.ownerDocument), opts.buttonClass);
6366                        }
6367                    }
6368                }
6369                // Fire input event for backwards compatibility if anyone was listening directly to the DOM input event
6370                if (this.options.targetBlank || opts.target === '_blank' || opts.buttonClass) {
6371                    customEvent = this.options.ownerDocument.createEvent('HTMLEvents');
6372                    customEvent.initEvent('input', true, true, this.options.contentWindow);
6373                    for (var i = 0; i < this.elements.length; i += 1) {
6374                        this.elements[i].dispatchEvent(customEvent);
6375                    }
6376                }
6377            } finally {
6378                this.events.enableCustomEvent('editableInput');
6379            }
6380            // Fire our custom editableInput event
6381            this.events.triggerCustomEvent('editableInput', customEvent, currentEditor);
6382        },
6383
6384        cleanPaste: function (text) {
6385            this.getExtensionByName('paste').cleanPaste(text);
6386        },
6387
6388        pasteHTML: function (html, options) {
6389            this.getExtensionByName('paste').pasteHTML(html, options);
6390        },
6391
6392        setContent: function (html, index) {
6393            index = index || 0;
6394
6395            if (this.elements[index]) {
6396                var target = this.elements[index];
6397                target.innerHTML = html;
6398                this.events.updateInput(target, { target: target, currentTarget: target });
6399            }
6400        }
6401    };
6402}());
6403
6404(function () {
6405    // summary: The default options hash used by the Editor
6406
6407    MediumEditor.prototype.defaults = {
6408        activeButtonClass: 'medium-editor-button-active',
6409        buttonLabels: false,
6410        delay: 0,
6411        disableReturn: false,
6412        disableDoubleReturn: false,
6413        disableEditing: false,
6414        autoLink: false,
6415        elementsContainer: false,
6416        contentWindow: window,
6417        ownerDocument: document,
6418        targetBlank: false,
6419        extensions: {},
6420        spellcheck: true
6421    };
6422})();
6423
6424MediumEditor.parseVersionString = function (release) {
6425    var split = release.split('-'),
6426        version = split[0].split('.'),
6427        preRelease = (split.length > 1) ? split[1] : '';
6428    return {
6429        major: parseInt(version[0], 10),
6430        minor: parseInt(version[1], 10),
6431        revision: parseInt(version[2], 10),
6432        preRelease: preRelease,
6433        toString: function () {
6434            return [version[0], version[1], version[2]].join('.') + (preRelease ? '-' + preRelease : '');
6435        }
6436    };
6437};
6438
6439MediumEditor.version = MediumEditor.parseVersionString.call(this, ({
6440    // grunt-bump looks for this:
6441    'version': '5.8.3'
6442}).version);
6443
6444    return MediumEditor;
6445}
6445()));

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.