shop.balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

summernote.js (210817B)


      1 /**
      2  * Super simple wysiwyg editor v0.8.2
      3  * http://summernote.org/
      4  *
      5  * summernote.js
      6  * Copyright 2013-2016 Alan Hong. and other contributors
      7  * summernote may be freely distributed under the MIT license./
      8  *
      9  * Date: 2016-08-07T05:11Z
     10  */
     11 (function (factory) {
     12   /* global define */
     13   if (typeof define === 'function' && define.amd) {
     14     // AMD. Register as an anonymous module.
     15     define(['jquery'], factory);
     16   } else if (typeof module === 'object' && module.exports) {
     17     // Node/CommonJS
     18     module.exports = factory(require('jquery'));
     19   } else {
     20     // Browser globals
     21     factory(window.jQuery);
     22   }
     23 }(function ($) {
     24   'use strict';
     25 
     26   /**
     27    * @class core.func
     28    *
     29    * func utils (for high-order func's arg)
     30    *
     31    * @singleton
     32    * @alternateClassName func
     33    */
     34   var func = (function () {
     35     var eq = function (itemA) {
     36       return function (itemB) {
     37         return itemA === itemB;
     38       };
     39     };
     40 
     41     var eq2 = function (itemA, itemB) {
     42       return itemA === itemB;
     43     };
     44 
     45     var peq2 = function (propName) {
     46       return function (itemA, itemB) {
     47         return itemA[propName] === itemB[propName];
     48       };
     49     };
     50 
     51     var ok = function () {
     52       return true;
     53     };
     54 
     55     var fail = function () {
     56       return false;
     57     };
     58 
     59     var not = function (f) {
     60       return function () {
     61         return !f.apply(f, arguments);
     62       };
     63     };
     64 
     65     var and = function (fA, fB) {
     66       return function (item) {
     67         return fA(item) && fB(item);
     68       };
     69     };
     70 
     71     var self = function (a) {
     72       return a;
     73     };
     74 
     75     var invoke = function (obj, method) {
     76       return function () {
     77         return obj[method].apply(obj, arguments);
     78       };
     79     };
     80 
     81     var idCounter = 0;
     82 
     83     /**
     84      * generate a globally-unique id
     85      *
     86      * @param {String} [prefix]
     87      */
     88     var uniqueId = function (prefix) {
     89       var id = ++idCounter + '';
     90       return prefix ? prefix + id : id;
     91     };
     92 
     93     /**
     94      * returns bnd (bounds) from rect
     95      *
     96      * - IE Compatibility Issue: http://goo.gl/sRLOAo
     97      * - Scroll Issue: http://goo.gl/sNjUc
     98      *
     99      * @param {Rect} rect
    100      * @return {Object} bounds
    101      * @return {Number} bounds.top
    102      * @return {Number} bounds.left
    103      * @return {Number} bounds.width
    104      * @return {Number} bounds.height
    105      */
    106     var rect2bnd = function (rect) {
    107       var $document = $(document);
    108       return {
    109         top: rect.top + $document.scrollTop(),
    110         left: rect.left + $document.scrollLeft(),
    111         width: rect.right - rect.left,
    112         height: rect.bottom - rect.top
    113       };
    114     };
    115 
    116     /**
    117      * returns a copy of the object where the keys have become the values and the values the keys.
    118      * @param {Object} obj
    119      * @return {Object}
    120      */
    121     var invertObject = function (obj) {
    122       var inverted = {};
    123       for (var key in obj) {
    124         if (obj.hasOwnProperty(key)) {
    125           inverted[obj[key]] = key;
    126         }
    127       }
    128       return inverted;
    129     };
    130 
    131     /**
    132      * @param {String} namespace
    133      * @param {String} [prefix]
    134      * @return {String}
    135      */
    136     var namespaceToCamel = function (namespace, prefix) {
    137       prefix = prefix || '';
    138       return prefix + namespace.split('.').map(function (name) {
    139         return name.substring(0, 1).toUpperCase() + name.substring(1);
    140       }).join('');
    141     };
    142 
    143     /**
    144      * Returns a function, that, as long as it continues to be invoked, will not
    145      * be triggered. The function will be called after it stops being called for
    146      * N milliseconds. If `immediate` is passed, trigger the function on the
    147      * leading edge, instead of the trailing.
    148      * @param {Function} func
    149      * @param {Number} wait
    150      * @param {Boolean} immediate
    151      * @return {Function}
    152      */
    153     var debounce = function (func, wait, immediate) {
    154       var timeout;
    155       return function () {
    156         var context = this, args = arguments;
    157         var later = function () {
    158           timeout = null;
    159           if (!immediate) {
    160             func.apply(context, args);
    161           }
    162         };
    163         var callNow = immediate && !timeout;
    164         clearTimeout(timeout);
    165         timeout = setTimeout(later, wait);
    166         if (callNow) {
    167           func.apply(context, args);
    168         }
    169       };
    170     };
    171 
    172     return {
    173       eq: eq,
    174       eq2: eq2,
    175       peq2: peq2,
    176       ok: ok,
    177       fail: fail,
    178       self: self,
    179       not: not,
    180       and: and,
    181       invoke: invoke,
    182       uniqueId: uniqueId,
    183       rect2bnd: rect2bnd,
    184       invertObject: invertObject,
    185       namespaceToCamel: namespaceToCamel,
    186       debounce: debounce
    187     };
    188   })();
    189 
    190   /**
    191    * @class core.list
    192    *
    193    * list utils
    194    *
    195    * @singleton
    196    * @alternateClassName list
    197    */
    198   var list = (function () {
    199     /**
    200      * returns the first item of an array.
    201      *
    202      * @param {Array} array
    203      */
    204     var head = function (array) {
    205       return array[0];
    206     };
    207 
    208     /**
    209      * returns the last item of an array.
    210      *
    211      * @param {Array} array
    212      */
    213     var last = function (array) {
    214       return array[array.length - 1];
    215     };
    216 
    217     /**
    218      * returns everything but the last entry of the array.
    219      *
    220      * @param {Array} array
    221      */
    222     var initial = function (array) {
    223       return array.slice(0, array.length - 1);
    224     };
    225 
    226     /**
    227      * returns the rest of the items in an array.
    228      *
    229      * @param {Array} array
    230      */
    231     var tail = function (array) {
    232       return array.slice(1);
    233     };
    234 
    235     /**
    236      * returns item of array
    237      */
    238     var find = function (array, pred) {
    239       for (var idx = 0, len = array.length; idx < len; idx ++) {
    240         var item = array[idx];
    241         if (pred(item)) {
    242           return item;
    243         }
    244       }
    245     };
    246 
    247     /**
    248      * returns true if all of the values in the array pass the predicate truth test.
    249      */
    250     var all = function (array, pred) {
    251       for (var idx = 0, len = array.length; idx < len; idx ++) {
    252         if (!pred(array[idx])) {
    253           return false;
    254         }
    255       }
    256       return true;
    257     };
    258 
    259     /**
    260      * returns index of item
    261      */
    262     var indexOf = function (array, item) {
    263       return $.inArray(item, array);
    264     };
    265 
    266     /**
    267      * returns true if the value is present in the list.
    268      */
    269     var contains = function (array, item) {
    270       return indexOf(array, item) !== -1;
    271     };
    272 
    273     /**
    274      * get sum from a list
    275      *
    276      * @param {Array} array - array
    277      * @param {Function} fn - iterator
    278      */
    279     var sum = function (array, fn) {
    280       fn = fn || func.self;
    281       return array.reduce(function (memo, v) {
    282         return memo + fn(v);
    283       }, 0);
    284     };
    285   
    286     /**
    287      * returns a copy of the collection with array type.
    288      * @param {Collection} collection - collection eg) node.childNodes, ...
    289      */
    290     var from = function (collection) {
    291       var result = [], idx = -1, length = collection.length;
    292       while (++idx < length) {
    293         result[idx] = collection[idx];
    294       }
    295       return result;
    296     };
    297 
    298     /**
    299      * returns whether list is empty or not
    300      */
    301     var isEmpty = function (array) {
    302       return !array || !array.length;
    303     };
    304   
    305     /**
    306      * cluster elements by predicate function.
    307      *
    308      * @param {Array} array - array
    309      * @param {Function} fn - predicate function for cluster rule
    310      * @param {Array[]}
    311      */
    312     var clusterBy = function (array, fn) {
    313       if (!array.length) { return []; }
    314       var aTail = tail(array);
    315       return aTail.reduce(function (memo, v) {
    316         var aLast = last(memo);
    317         if (fn(last(aLast), v)) {
    318           aLast[aLast.length] = v;
    319         } else {
    320           memo[memo.length] = [v];
    321         }
    322         return memo;
    323       }, [[head(array)]]);
    324     };
    325   
    326     /**
    327      * returns a copy of the array with all false values removed
    328      *
    329      * @param {Array} array - array
    330      * @param {Function} fn - predicate function for cluster rule
    331      */
    332     var compact = function (array) {
    333       var aResult = [];
    334       for (var idx = 0, len = array.length; idx < len; idx ++) {
    335         if (array[idx]) { aResult.push(array[idx]); }
    336       }
    337       return aResult;
    338     };
    339 
    340     /**
    341      * produces a duplicate-free version of the array
    342      *
    343      * @param {Array} array
    344      */
    345     var unique = function (array) {
    346       var results = [];
    347 
    348       for (var idx = 0, len = array.length; idx < len; idx ++) {
    349         if (!contains(results, array[idx])) {
    350           results.push(array[idx]);
    351         }
    352       }
    353 
    354       return results;
    355     };
    356 
    357     /**
    358      * returns next item.
    359      * @param {Array} array
    360      */
    361     var next = function (array, item) {
    362       var idx = indexOf(array, item);
    363       if (idx === -1) { return null; }
    364 
    365       return array[idx + 1];
    366     };
    367 
    368     /**
    369      * returns prev item.
    370      * @param {Array} array
    371      */
    372     var prev = function (array, item) {
    373       var idx = indexOf(array, item);
    374       if (idx === -1) { return null; }
    375 
    376       return array[idx - 1];
    377     };
    378 
    379     return { head: head, last: last, initial: initial, tail: tail,
    380              prev: prev, next: next, find: find, contains: contains,
    381              all: all, sum: sum, from: from, isEmpty: isEmpty,
    382              clusterBy: clusterBy, compact: compact, unique: unique };
    383   })();
    384 
    385   var isSupportAmd = typeof define === 'function' && define.amd;
    386 
    387   /**
    388    * returns whether font is installed or not.
    389    *
    390    * @param {String} fontName
    391    * @return {Boolean}
    392    */
    393   var isFontInstalled = function (fontName) {
    394     var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
    395     var $tester = $('<div>').css({
    396       position: 'absolute',
    397       left: '-9999px',
    398       top: '-9999px',
    399       fontSize: '200px'
    400     }).text('mmmmmmmmmwwwwwww').appendTo(document.body);
    401 
    402     var originalWidth = $tester.css('fontFamily', testFontName).width();
    403     var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
    404 
    405     $tester.remove();
    406 
    407     return originalWidth !== width;
    408   };
    409 
    410   var userAgent = navigator.userAgent;
    411   var isMSIE = /MSIE|Trident/i.test(userAgent);
    412   var browserVersion;
    413   if (isMSIE) {
    414     var matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
    415     if (matches) {
    416       browserVersion = parseFloat(matches[1]);
    417     }
    418     matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent);
    419     if (matches) {
    420       browserVersion = parseFloat(matches[1]);
    421     }
    422   }
    423 
    424   var isEdge = /Edge\/\d+/.test(userAgent);
    425 
    426   var hasCodeMirror = !!window.CodeMirror;
    427   if (!hasCodeMirror && isSupportAmd && typeof require !== 'undefined') {
    428     if (typeof require.resolve !== 'undefined') {
    429       try {
    430         // If CodeMirror can't be resolved, `require.resolve` will throw an
    431         // exception and `hasCodeMirror` won't be set to `true`.
    432         require.resolve('codemirror');
    433         hasCodeMirror = true;
    434       } catch (e) {
    435         // Do nothing.
    436       }
    437     } else if (typeof eval('require').specified !== 'undefined') {
    438       hasCodeMirror = eval('require').specified('codemirror');
    439     }
    440   }
    441 
    442   /**
    443    * @class core.agent
    444    *
    445    * Object which check platform and agent
    446    *
    447    * @singleton
    448    * @alternateClassName agent
    449    */
    450   var agent = {
    451     isMac: navigator.appVersion.indexOf('Mac') > -1,
    452     isMSIE: isMSIE,
    453     isEdge: isEdge,
    454     isFF: !isEdge && /firefox/i.test(userAgent),
    455     isPhantom: /PhantomJS/i.test(userAgent),
    456     isWebkit: !isEdge && /webkit/i.test(userAgent),
    457     isChrome: !isEdge && /chrome/i.test(userAgent),
    458     isSafari: !isEdge && /safari/i.test(userAgent),
    459     browserVersion: browserVersion,
    460     jqueryVersion: parseFloat($.fn.jquery),
    461     isSupportAmd: isSupportAmd,
    462     hasCodeMirror: hasCodeMirror,
    463     isFontInstalled: isFontInstalled,
    464     isW3CRangeSupport: !!document.createRange
    465   };
    466 
    467 
    468   var NBSP_CHAR = String.fromCharCode(160);
    469   var ZERO_WIDTH_NBSP_CHAR = '\ufeff';
    470 
    471   /**
    472    * @class core.dom
    473    *
    474    * Dom functions
    475    *
    476    * @singleton
    477    * @alternateClassName dom
    478    */
    479   var dom = (function () {
    480     /**
    481      * @method isEditable
    482      *
    483      * returns whether node is `note-editable` or not.
    484      *
    485      * @param {Node} node
    486      * @return {Boolean}
    487      */
    488     var isEditable = function (node) {
    489       return node && $(node).hasClass('note-editable');
    490     };
    491 
    492     /**
    493      * @method isControlSizing
    494      *
    495      * returns whether node is `note-control-sizing` or not.
    496      *
    497      * @param {Node} node
    498      * @return {Boolean}
    499      */
    500     var isControlSizing = function (node) {
    501       return node && $(node).hasClass('note-control-sizing');
    502     };
    503 
    504     /**
    505      * @method makePredByNodeName
    506      *
    507      * returns predicate which judge whether nodeName is same
    508      *
    509      * @param {String} nodeName
    510      * @return {Function}
    511      */
    512     var makePredByNodeName = function (nodeName) {
    513       nodeName = nodeName.toUpperCase();
    514       return function (node) {
    515         return node && node.nodeName.toUpperCase() === nodeName;
    516       };
    517     };
    518 
    519     /**
    520      * @method isText
    521      *
    522      *
    523      *
    524      * @param {Node} node
    525      * @return {Boolean} true if node's type is text(3)
    526      */
    527     var isText = function (node) {
    528       return node && node.nodeType === 3;
    529     };
    530 
    531     /**
    532      * @method isElement
    533      *
    534      *
    535      *
    536      * @param {Node} node
    537      * @return {Boolean} true if node's type is element(1)
    538      */
    539     var isElement = function (node) {
    540       return node && node.nodeType === 1;
    541     };
    542 
    543     /**
    544      * ex) br, col, embed, hr, img, input, ...
    545      * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
    546      */
    547     var isVoid = function (node) {
    548       return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON/.test(node.nodeName.toUpperCase());
    549     };
    550 
    551     var isPara = function (node) {
    552       if (isEditable(node)) {
    553         return false;
    554       }
    555 
    556       // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph
    557       return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
    558     };
    559 
    560     var isHeading = function (node) {
    561       return node && /^H[1-7]/.test(node.nodeName.toUpperCase());
    562     };
    563 
    564     var isPre = makePredByNodeName('PRE');
    565 
    566     var isLi = makePredByNodeName('LI');
    567 
    568     var isPurePara = function (node) {
    569       return isPara(node) && !isLi(node);
    570     };
    571 
    572     var isTable = makePredByNodeName('TABLE');
    573 
    574     var isData = makePredByNodeName('DATA');
    575 
    576     var isInline = function (node) {
    577       return !isBodyContainer(node) &&
    578              !isList(node) &&
    579              !isHr(node) &&
    580              !isPara(node) &&
    581              !isTable(node) &&
    582              !isBlockquote(node) &&
    583              !isData(node);
    584     };
    585 
    586     var isList = function (node) {
    587       return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
    588     };
    589 
    590     var isHr = makePredByNodeName('HR');
    591 
    592     var isCell = function (node) {
    593       return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
    594     };
    595 
    596     var isBlockquote = makePredByNodeName('BLOCKQUOTE');
    597 
    598     var isBodyContainer = function (node) {
    599       return isCell(node) || isBlockquote(node) || isEditable(node);
    600     };
    601 
    602     var isAnchor = makePredByNodeName('A');
    603 
    604     var isParaInline = function (node) {
    605       return isInline(node) && !!ancestor(node, isPara);
    606     };
    607 
    608     var isBodyInline = function (node) {
    609       return isInline(node) && !ancestor(node, isPara);
    610     };
    611 
    612     var isBody = makePredByNodeName('BODY');
    613 
    614     /**
    615      * returns whether nodeB is closest sibling of nodeA
    616      *
    617      * @param {Node} nodeA
    618      * @param {Node} nodeB
    619      * @return {Boolean}
    620      */
    621     var isClosestSibling = function (nodeA, nodeB) {
    622       return nodeA.nextSibling === nodeB ||
    623              nodeA.previousSibling === nodeB;
    624     };
    625 
    626     /**
    627      * returns array of closest siblings with node
    628      *
    629      * @param {Node} node
    630      * @param {function} [pred] - predicate function
    631      * @return {Node[]}
    632      */
    633     var withClosestSiblings = function (node, pred) {
    634       pred = pred || func.ok;
    635 
    636       var siblings = [];
    637       if (node.previousSibling && pred(node.previousSibling)) {
    638         siblings.push(node.previousSibling);
    639       }
    640       siblings.push(node);
    641       if (node.nextSibling && pred(node.nextSibling)) {
    642         siblings.push(node.nextSibling);
    643       }
    644       return siblings;
    645     };
    646 
    647     /**
    648      * blank HTML for cursor position
    649      * - [workaround] old IE only works with &nbsp;
    650      * - [workaround] IE11 and other browser works with bogus br
    651      */
    652     var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? '&nbsp;' : '<br>';
    653 
    654     /**
    655      * @method nodeLength
    656      *
    657      * returns #text's text size or element's childNodes size
    658      *
    659      * @param {Node} node
    660      */
    661     var nodeLength = function (node) {
    662       if (isText(node)) {
    663         return node.nodeValue.length;
    664       }
    665       
    666       if (node) {
    667         return node.childNodes.length;
    668       }
    669       
    670       return 0;
    671       
    672     };
    673 
    674     /**
    675      * returns whether node is empty or not.
    676      *
    677      * @param {Node} node
    678      * @return {Boolean}
    679      */
    680     var isEmpty = function (node) {
    681       var len = nodeLength(node);
    682 
    683       if (len === 0) {
    684         return true;
    685       } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) {
    686         // ex) <p><br></p>, <span><br></span>
    687         return true;
    688       } else if (list.all(node.childNodes, isText) && node.innerHTML === '') {
    689         // ex) <p></p>, <span></span>
    690         return true;
    691       }
    692 
    693       return false;
    694     };
    695 
    696     /**
    697      * padding blankHTML if node is empty (for cursor position)
    698      */
    699     var paddingBlankHTML = function (node) {
    700       if (!isVoid(node) && !nodeLength(node)) {
    701         node.innerHTML = blankHTML;
    702       }
    703     };
    704 
    705     /**
    706      * find nearest ancestor predicate hit
    707      *
    708      * @param {Node} node
    709      * @param {Function} pred - predicate function
    710      */
    711     var ancestor = function (node, pred) {
    712       while (node) {
    713         if (pred(node)) { return node; }
    714         if (isEditable(node)) { break; }
    715 
    716         node = node.parentNode;
    717       }
    718       return null;
    719     };
    720 
    721     /**
    722      * find nearest ancestor only single child blood line and predicate hit
    723      *
    724      * @param {Node} node
    725      * @param {Function} pred - predicate function
    726      */
    727     var singleChildAncestor = function (node, pred) {
    728       node = node.parentNode;
    729 
    730       while (node) {
    731         if (nodeLength(node) !== 1) { break; }
    732         if (pred(node)) { return node; }
    733         if (isEditable(node)) { break; }
    734 
    735         node = node.parentNode;
    736       }
    737       return null;
    738     };
    739 
    740     /**
    741      * returns new array of ancestor nodes (until predicate hit).
    742      *
    743      * @param {Node} node
    744      * @param {Function} [optional] pred - predicate function
    745      */
    746     var listAncestor = function (node, pred) {
    747       pred = pred || func.fail;
    748 
    749       var ancestors = [];
    750       ancestor(node, function (el) {
    751         if (!isEditable(el)) {
    752           ancestors.push(el);
    753         }
    754 
    755         return pred(el);
    756       });
    757       return ancestors;
    758     };
    759 
    760     /**
    761      * find farthest ancestor predicate hit
    762      */
    763     var lastAncestor = function (node, pred) {
    764       var ancestors = listAncestor(node);
    765       return list.last(ancestors.filter(pred));
    766     };
    767 
    768     /**
    769      * returns common ancestor node between two nodes.
    770      *
    771      * @param {Node} nodeA
    772      * @param {Node} nodeB
    773      */
    774     var commonAncestor = function (nodeA, nodeB) {
    775       var ancestors = listAncestor(nodeA);
    776       for (var n = nodeB; n; n = n.parentNode) {
    777         if ($.inArray(n, ancestors) > -1) { return n; }
    778       }
    779       return null; // difference document area
    780     };
    781 
    782     /**
    783      * listing all previous siblings (until predicate hit).
    784      *
    785      * @param {Node} node
    786      * @param {Function} [optional] pred - predicate function
    787      */
    788     var listPrev = function (node, pred) {
    789       pred = pred || func.fail;
    790 
    791       var nodes = [];
    792       while (node) {
    793         if (pred(node)) { break; }
    794         nodes.push(node);
    795         node = node.previousSibling;
    796       }
    797       return nodes;
    798     };
    799 
    800     /**
    801      * listing next siblings (until predicate hit).
    802      *
    803      * @param {Node} node
    804      * @param {Function} [pred] - predicate function
    805      */
    806     var listNext = function (node, pred) {
    807       pred = pred || func.fail;
    808 
    809       var nodes = [];
    810       while (node) {
    811         if (pred(node)) { break; }
    812         nodes.push(node);
    813         node = node.nextSibling;
    814       }
    815       return nodes;
    816     };
    817 
    818     /**
    819      * listing descendant nodes
    820      *
    821      * @param {Node} node
    822      * @param {Function} [pred] - predicate function
    823      */
    824     var listDescendant = function (node, pred) {
    825       var descendants = [];
    826       pred = pred || func.ok;
    827 
    828       // start DFS(depth first search) with node
    829       (function fnWalk(current) {
    830         if (node !== current && pred(current)) {
    831           descendants.push(current);
    832         }
    833         for (var idx = 0, len = current.childNodes.length; idx < len; idx++) {
    834           fnWalk(current.childNodes[idx]);
    835         }
    836       })(node);
    837 
    838       return descendants;
    839     };
    840 
    841     /**
    842      * wrap node with new tag.
    843      *
    844      * @param {Node} node
    845      * @param {Node} tagName of wrapper
    846      * @return {Node} - wrapper
    847      */
    848     var wrap = function (node, wrapperName) {
    849       var parent = node.parentNode;
    850       var wrapper = $('<' + wrapperName + '>')[0];
    851 
    852       parent.insertBefore(wrapper, node);
    853       wrapper.appendChild(node);
    854 
    855       return wrapper;
    856     };
    857 
    858     /**
    859      * insert node after preceding
    860      *
    861      * @param {Node} node
    862      * @param {Node} preceding - predicate function
    863      */
    864     var insertAfter = function (node, preceding) {
    865       var next = preceding.nextSibling, parent = preceding.parentNode;
    866       if (next) {
    867         parent.insertBefore(node, next);
    868       } else {
    869         parent.appendChild(node);
    870       }
    871       return node;
    872     };
    873 
    874     /**
    875      * append elements.
    876      *
    877      * @param {Node} node
    878      * @param {Collection} aChild
    879      */
    880     var appendChildNodes = function (node, aChild) {
    881       $.each(aChild, function (idx, child) {
    882         node.appendChild(child);
    883       });
    884       return node;
    885     };
    886 
    887     /**
    888      * returns whether boundaryPoint is left edge or not.
    889      *
    890      * @param {BoundaryPoint} point
    891      * @return {Boolean}
    892      */
    893     var isLeftEdgePoint = function (point) {
    894       return point.offset === 0;
    895     };
    896 
    897     /**
    898      * returns whether boundaryPoint is right edge or not.
    899      *
    900      * @param {BoundaryPoint} point
    901      * @return {Boolean}
    902      */
    903     var isRightEdgePoint = function (point) {
    904       return point.offset === nodeLength(point.node);
    905     };
    906 
    907     /**
    908      * returns whether boundaryPoint is edge or not.
    909      *
    910      * @param {BoundaryPoint} point
    911      * @return {Boolean}
    912      */
    913     var isEdgePoint = function (point) {
    914       return isLeftEdgePoint(point) || isRightEdgePoint(point);
    915     };
    916 
    917     /**
    918      * returns whether node is left edge of ancestor or not.
    919      *
    920      * @param {Node} node
    921      * @param {Node} ancestor
    922      * @return {Boolean}
    923      */
    924     var isLeftEdgeOf = function (node, ancestor) {
    925       while (node && node !== ancestor) {
    926         if (position(node) !== 0) {
    927           return false;
    928         }
    929         node = node.parentNode;
    930       }
    931 
    932       return true;
    933     };
    934 
    935     /**
    936      * returns whether node is right edge of ancestor or not.
    937      *
    938      * @param {Node} node
    939      * @param {Node} ancestor
    940      * @return {Boolean}
    941      */
    942     var isRightEdgeOf = function (node, ancestor) {
    943       if (!ancestor) {
    944         return false;
    945       }
    946       while (node && node !== ancestor) {
    947         if (position(node) !== nodeLength(node.parentNode) - 1) {
    948           return false;
    949         }
    950         node = node.parentNode;
    951       }
    952 
    953       return true;
    954     };
    955 
    956     /**
    957      * returns whether point is left edge of ancestor or not.
    958      * @param {BoundaryPoint} point
    959      * @param {Node} ancestor
    960      * @return {Boolean}
    961      */
    962     var isLeftEdgePointOf = function (point, ancestor) {
    963       return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor);
    964     };
    965 
    966     /**
    967      * returns whether point is right edge of ancestor or not.
    968      * @param {BoundaryPoint} point
    969      * @param {Node} ancestor
    970      * @return {Boolean}
    971      */
    972     var isRightEdgePointOf = function (point, ancestor) {
    973       return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor);
    974     };
    975 
    976     /**
    977      * returns offset from parent.
    978      *
    979      * @param {Node} node
    980      */
    981     var position = function (node) {
    982       var offset = 0;
    983       while ((node = node.previousSibling)) {
    984         offset += 1;
    985       }
    986       return offset;
    987     };
    988 
    989     var hasChildren = function (node) {
    990       return !!(node && node.childNodes && node.childNodes.length);
    991     };
    992 
    993     /**
    994      * returns previous boundaryPoint
    995      *
    996      * @param {BoundaryPoint} point
    997      * @param {Boolean} isSkipInnerOffset
    998      * @return {BoundaryPoint}
    999      */
   1000     var prevPoint = function (point, isSkipInnerOffset) {
   1001       var node, offset;
   1002 
   1003       if (point.offset === 0) {
   1004         if (isEditable(point.node)) {
   1005           return null;
   1006         }
   1007 
   1008         node = point.node.parentNode;
   1009         offset = position(point.node);
   1010       } else if (hasChildren(point.node)) {
   1011         node = point.node.childNodes[point.offset - 1];
   1012         offset = nodeLength(node);
   1013       } else {
   1014         node = point.node;
   1015         offset = isSkipInnerOffset ? 0 : point.offset - 1;
   1016       }
   1017 
   1018       return {
   1019         node: node,
   1020         offset: offset
   1021       };
   1022     };
   1023 
   1024     /**
   1025      * returns next boundaryPoint
   1026      *
   1027      * @param {BoundaryPoint} point
   1028      * @param {Boolean} isSkipInnerOffset
   1029      * @return {BoundaryPoint}
   1030      */
   1031     var nextPoint = function (point, isSkipInnerOffset) {
   1032       var node, offset;
   1033 
   1034       if (nodeLength(point.node) === point.offset) {
   1035         if (isEditable(point.node)) {
   1036           return null;
   1037         }
   1038 
   1039         node = point.node.parentNode;
   1040         offset = position(point.node) + 1;
   1041       } else if (hasChildren(point.node)) {
   1042         node = point.node.childNodes[point.offset];
   1043         offset = 0;
   1044       } else {
   1045         node = point.node;
   1046         offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1;
   1047       }
   1048 
   1049       return {
   1050         node: node,
   1051         offset: offset
   1052       };
   1053     };
   1054 
   1055     /**
   1056      * returns whether pointA and pointB is same or not.
   1057      *
   1058      * @param {BoundaryPoint} pointA
   1059      * @param {BoundaryPoint} pointB
   1060      * @return {Boolean}
   1061      */
   1062     var isSamePoint = function (pointA, pointB) {
   1063       return pointA.node === pointB.node && pointA.offset === pointB.offset;
   1064     };
   1065 
   1066     /**
   1067      * returns whether point is visible (can set cursor) or not.
   1068      *
   1069      * @param {BoundaryPoint} point
   1070      * @return {Boolean}
   1071      */
   1072     var isVisiblePoint = function (point) {
   1073       if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) {
   1074         return true;
   1075       }
   1076 
   1077       var leftNode = point.node.childNodes[point.offset - 1];
   1078       var rightNode = point.node.childNodes[point.offset];
   1079       if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) {
   1080         return true;
   1081       }
   1082 
   1083       return false;
   1084     };
   1085 
   1086     /**
   1087      * @method prevPointUtil
   1088      *
   1089      * @param {BoundaryPoint} point
   1090      * @param {Function} pred
   1091      * @return {BoundaryPoint}
   1092      */
   1093     var prevPointUntil = function (point, pred) {
   1094       while (point) {
   1095         if (pred(point)) {
   1096           return point;
   1097         }
   1098 
   1099         point = prevPoint(point);
   1100       }
   1101 
   1102       return null;
   1103     };
   1104 
   1105     /**
   1106      * @method nextPointUntil
   1107      *
   1108      * @param {BoundaryPoint} point
   1109      * @param {Function} pred
   1110      * @return {BoundaryPoint}
   1111      */
   1112     var nextPointUntil = function (point, pred) {
   1113       while (point) {
   1114         if (pred(point)) {
   1115           return point;
   1116         }
   1117 
   1118         point = nextPoint(point);
   1119       }
   1120 
   1121       return null;
   1122     };
   1123 
   1124     /**
   1125      * returns whether point has character or not.
   1126      *
   1127      * @param {Point} point
   1128      * @return {Boolean}
   1129      */
   1130     var isCharPoint = function (point) {
   1131       if (!isText(point.node)) {
   1132         return false;
   1133       }
   1134 
   1135       var ch = point.node.nodeValue.charAt(point.offset - 1);
   1136       return ch && (ch !== ' ' && ch !== NBSP_CHAR);
   1137     };
   1138 
   1139     /**
   1140      * @method walkPoint
   1141      *
   1142      * @param {BoundaryPoint} startPoint
   1143      * @param {BoundaryPoint} endPoint
   1144      * @param {Function} handler
   1145      * @param {Boolean} isSkipInnerOffset
   1146      */
   1147     var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) {
   1148       var point = startPoint;
   1149 
   1150       while (point) {
   1151         handler(point);
   1152 
   1153         if (isSamePoint(point, endPoint)) {
   1154           break;
   1155         }
   1156 
   1157         var isSkipOffset = isSkipInnerOffset &&
   1158                            startPoint.node !== point.node &&
   1159                            endPoint.node !== point.node;
   1160         point = nextPoint(point, isSkipOffset);
   1161       }
   1162     };
   1163 
   1164     /**
   1165      * @method makeOffsetPath
   1166      *
   1167      * return offsetPath(array of offset) from ancestor
   1168      *
   1169      * @param {Node} ancestor - ancestor node
   1170      * @param {Node} node
   1171      */
   1172     var makeOffsetPath = function (ancestor, node) {
   1173       var ancestors = listAncestor(node, func.eq(ancestor));
   1174       return ancestors.map(position).reverse();
   1175     };
   1176 
   1177     /**
   1178      * @method fromOffsetPath
   1179      *
   1180      * return element from offsetPath(array of offset)
   1181      *
   1182      * @param {Node} ancestor - ancestor node
   1183      * @param {array} offsets - offsetPath
   1184      */
   1185     var fromOffsetPath = function (ancestor, offsets) {
   1186       var current = ancestor;
   1187       for (var i = 0, len = offsets.length; i < len; i++) {
   1188         if (current.childNodes.length <= offsets[i]) {
   1189           current = current.childNodes[current.childNodes.length - 1];
   1190         } else {
   1191           current = current.childNodes[offsets[i]];
   1192         }
   1193       }
   1194       return current;
   1195     };
   1196 
   1197     /**
   1198      * @method splitNode
   1199      *
   1200      * split element or #text
   1201      *
   1202      * @param {BoundaryPoint} point
   1203      * @param {Object} [options]
   1204      * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
   1205      * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
   1206      * @return {Node} right node of boundaryPoint
   1207      */
   1208     var splitNode = function (point, options) {
   1209       var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML;
   1210       var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint;
   1211 
   1212       // edge case
   1213       if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) {
   1214         if (isLeftEdgePoint(point)) {
   1215           return point.node;
   1216         } else if (isRightEdgePoint(point)) {
   1217           return point.node.nextSibling;
   1218         }
   1219       }
   1220 
   1221       // split #text
   1222       if (isText(point.node)) {
   1223         return point.node.splitText(point.offset);
   1224       } else {
   1225         var childNode = point.node.childNodes[point.offset];
   1226         var clone = insertAfter(point.node.cloneNode(false), point.node);
   1227         appendChildNodes(clone, listNext(childNode));
   1228 
   1229         if (!isSkipPaddingBlankHTML) {
   1230           paddingBlankHTML(point.node);
   1231           paddingBlankHTML(clone);
   1232         }
   1233 
   1234         return clone;
   1235       }
   1236     };
   1237 
   1238     /**
   1239      * @method splitTree
   1240      *
   1241      * split tree by point
   1242      *
   1243      * @param {Node} root - split root
   1244      * @param {BoundaryPoint} point
   1245      * @param {Object} [options]
   1246      * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
   1247      * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
   1248      * @return {Node} right node of boundaryPoint
   1249      */
   1250     var splitTree = function (root, point, options) {
   1251       // ex) [#text, <span>, <p>]
   1252       var ancestors = listAncestor(point.node, func.eq(root));
   1253 
   1254       if (!ancestors.length) {
   1255         return null;
   1256       } else if (ancestors.length === 1) {
   1257         return splitNode(point, options);
   1258       }
   1259 
   1260       return ancestors.reduce(function (node, parent) {
   1261         if (node === point.node) {
   1262           node = splitNode(point, options);
   1263         }
   1264 
   1265         return splitNode({
   1266           node: parent,
   1267           offset: node ? dom.position(node) : nodeLength(parent)
   1268         }, options);
   1269       });
   1270     };
   1271 
   1272     /**
   1273      * split point
   1274      *
   1275      * @param {Point} point
   1276      * @param {Boolean} isInline
   1277      * @return {Object}
   1278      */
   1279     var splitPoint = function (point, isInline) {
   1280       // find splitRoot, container
   1281       //  - inline: splitRoot is a child of paragraph
   1282       //  - block: splitRoot is a child of bodyContainer
   1283       var pred = isInline ? isPara : isBodyContainer;
   1284       var ancestors = listAncestor(point.node, pred);
   1285       var topAncestor = list.last(ancestors) || point.node;
   1286 
   1287       var splitRoot, container;
   1288       if (pred(topAncestor)) {
   1289         splitRoot = ancestors[ancestors.length - 2];
   1290         container = topAncestor;
   1291       } else {
   1292         splitRoot = topAncestor;
   1293         container = splitRoot.parentNode;
   1294       }
   1295 
   1296       // if splitRoot is exists, split with splitTree
   1297       var pivot = splitRoot && splitTree(splitRoot, point, {
   1298         isSkipPaddingBlankHTML: isInline,
   1299         isNotSplitEdgePoint: isInline
   1300       });
   1301 
   1302       // if container is point.node, find pivot with point.offset
   1303       if (!pivot && container === point.node) {
   1304         pivot = point.node.childNodes[point.offset];
   1305       }
   1306 
   1307       return {
   1308         rightNode: pivot,
   1309         container: container
   1310       };
   1311     };
   1312 
   1313     var create = function (nodeName) {
   1314       return document.createElement(nodeName);
   1315     };
   1316 
   1317     var createText = function (text) {
   1318       return document.createTextNode(text);
   1319     };
   1320 
   1321     /**
   1322      * @method remove
   1323      *
   1324      * remove node, (isRemoveChild: remove child or not)
   1325      *
   1326      * @param {Node} node
   1327      * @param {Boolean} isRemoveChild
   1328      */
   1329     var remove = function (node, isRemoveChild) {
   1330       if (!node || !node.parentNode) { return; }
   1331       if (node.removeNode) { return node.removeNode(isRemoveChild); }
   1332 
   1333       var parent = node.parentNode;
   1334       if (!isRemoveChild) {
   1335         var nodes = [];
   1336         var i, len;
   1337         for (i = 0, len = node.childNodes.length; i < len; i++) {
   1338           nodes.push(node.childNodes[i]);
   1339         }
   1340 
   1341         for (i = 0, len = nodes.length; i < len; i++) {
   1342           parent.insertBefore(nodes[i], node);
   1343         }
   1344       }
   1345 
   1346       parent.removeChild(node);
   1347     };
   1348 
   1349     /**
   1350      * @method removeWhile
   1351      *
   1352      * @param {Node} node
   1353      * @param {Function} pred
   1354      */
   1355     var removeWhile = function (node, pred) {
   1356       while (node) {
   1357         if (isEditable(node) || !pred(node)) {
   1358           break;
   1359         }
   1360 
   1361         var parent = node.parentNode;
   1362         remove(node);
   1363         node = parent;
   1364       }
   1365     };
   1366 
   1367     /**
   1368      * @method replace
   1369      *
   1370      * replace node with provided nodeName
   1371      *
   1372      * @param {Node} node
   1373      * @param {String} nodeName
   1374      * @return {Node} - new node
   1375      */
   1376     var replace = function (node, nodeName) {
   1377       if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) {
   1378         return node;
   1379       }
   1380 
   1381       var newNode = create(nodeName);
   1382 
   1383       if (node.style.cssText) {
   1384         newNode.style.cssText = node.style.cssText;
   1385       }
   1386 
   1387       appendChildNodes(newNode, list.from(node.childNodes));
   1388       insertAfter(newNode, node);
   1389       remove(node);
   1390 
   1391       return newNode;
   1392     };
   1393 
   1394     var isTextarea = makePredByNodeName('TEXTAREA');
   1395 
   1396     /**
   1397      * @param {jQuery} $node
   1398      * @param {Boolean} [stripLinebreaks] - default: false
   1399      */
   1400     var value = function ($node, stripLinebreaks) {
   1401       var val = isTextarea($node[0]) ? $node.val() : $node.html();
   1402       if (stripLinebreaks) {
   1403         return val.replace(/[\n\r]/g, '');
   1404       }
   1405       return val;
   1406     };
   1407 
   1408     /**
   1409      * @method html
   1410      *
   1411      * get the HTML contents of node
   1412      *
   1413      * @param {jQuery} $node
   1414      * @param {Boolean} [isNewlineOnBlock]
   1415      */
   1416     var html = function ($node, isNewlineOnBlock) {
   1417       var markup = value($node);
   1418 
   1419       if (isNewlineOnBlock) {
   1420         var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g;
   1421         markup = markup.replace(regexTag, function (match, endSlash, name) {
   1422           name = name.toUpperCase();
   1423           var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) &&
   1424                                        !!endSlash;
   1425           var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name);
   1426 
   1427           return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : '');
   1428         });
   1429         markup = $.trim(markup);
   1430       }
   1431 
   1432       return markup;
   1433     };
   1434 
   1435     var posFromPlaceholder = function (placeholder) {
   1436       var $placeholder = $(placeholder);
   1437       var pos = $placeholder.offset();
   1438       var height = $placeholder.outerHeight(true); // include margin
   1439 
   1440       return {
   1441         left: pos.left,
   1442         top: pos.top + height
   1443       };
   1444     };
   1445 
   1446     var attachEvents = function ($node, events) {
   1447       Object.keys(events).forEach(function (key) {
   1448         $node.on(key, events[key]);
   1449       });
   1450     };
   1451 
   1452     var detachEvents = function ($node, events) {
   1453       Object.keys(events).forEach(function (key) {
   1454         $node.off(key, events[key]);
   1455       });
   1456     };
   1457 
   1458     return {
   1459       /** @property {String} NBSP_CHAR */
   1460       NBSP_CHAR: NBSP_CHAR,
   1461       /** @property {String} ZERO_WIDTH_NBSP_CHAR */
   1462       ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR,
   1463       /** @property {String} blank */
   1464       blank: blankHTML,
   1465       /** @property {String} emptyPara */
   1466       emptyPara: '<p>' + blankHTML + '</p>',
   1467       makePredByNodeName: makePredByNodeName,
   1468       isEditable: isEditable,
   1469       isControlSizing: isControlSizing,
   1470       isText: isText,
   1471       isElement: isElement,
   1472       isVoid: isVoid,
   1473       isPara: isPara,
   1474       isPurePara: isPurePara,
   1475       isHeading: isHeading,
   1476       isInline: isInline,
   1477       isBlock: func.not(isInline),
   1478       isBodyInline: isBodyInline,
   1479       isBody: isBody,
   1480       isParaInline: isParaInline,
   1481       isPre: isPre,
   1482       isList: isList,
   1483       isTable: isTable,
   1484       isData: isData,
   1485       isCell: isCell,
   1486       isBlockquote: isBlockquote,
   1487       isBodyContainer: isBodyContainer,
   1488       isAnchor: isAnchor,
   1489       isDiv: makePredByNodeName('DIV'),
   1490       isLi: isLi,
   1491       isBR: makePredByNodeName('BR'),
   1492       isSpan: makePredByNodeName('SPAN'),
   1493       isB: makePredByNodeName('B'),
   1494       isU: makePredByNodeName('U'),
   1495       isS: makePredByNodeName('S'),
   1496       isI: makePredByNodeName('I'),
   1497       isImg: makePredByNodeName('IMG'),
   1498       isTextarea: isTextarea,
   1499       isEmpty: isEmpty,
   1500       isEmptyAnchor: func.and(isAnchor, isEmpty),
   1501       isClosestSibling: isClosestSibling,
   1502       withClosestSiblings: withClosestSiblings,
   1503       nodeLength: nodeLength,
   1504       isLeftEdgePoint: isLeftEdgePoint,
   1505       isRightEdgePoint: isRightEdgePoint,
   1506       isEdgePoint: isEdgePoint,
   1507       isLeftEdgeOf: isLeftEdgeOf,
   1508       isRightEdgeOf: isRightEdgeOf,
   1509       isLeftEdgePointOf: isLeftEdgePointOf,
   1510       isRightEdgePointOf: isRightEdgePointOf,
   1511       prevPoint: prevPoint,
   1512       nextPoint: nextPoint,
   1513       isSamePoint: isSamePoint,
   1514       isVisiblePoint: isVisiblePoint,
   1515       prevPointUntil: prevPointUntil,
   1516       nextPointUntil: nextPointUntil,
   1517       isCharPoint: isCharPoint,
   1518       walkPoint: walkPoint,
   1519       ancestor: ancestor,
   1520       singleChildAncestor: singleChildAncestor,
   1521       listAncestor: listAncestor,
   1522       lastAncestor: lastAncestor,
   1523       listNext: listNext,
   1524       listPrev: listPrev,
   1525       listDescendant: listDescendant,
   1526       commonAncestor: commonAncestor,
   1527       wrap: wrap,
   1528       insertAfter: insertAfter,
   1529       appendChildNodes: appendChildNodes,
   1530       position: position,
   1531       hasChildren: hasChildren,
   1532       makeOffsetPath: makeOffsetPath,
   1533       fromOffsetPath: fromOffsetPath,
   1534       splitTree: splitTree,
   1535       splitPoint: splitPoint,
   1536       create: create,
   1537       createText: createText,
   1538       remove: remove,
   1539       removeWhile: removeWhile,
   1540       replace: replace,
   1541       html: html,
   1542       value: value,
   1543       posFromPlaceholder: posFromPlaceholder,
   1544       attachEvents: attachEvents,
   1545       detachEvents: detachEvents
   1546     };
   1547   })();
   1548 
   1549   /**
   1550    * @param {jQuery} $note
   1551    * @param {Object} options
   1552    * @return {Context}
   1553    */
   1554   var Context = function ($note, options) {
   1555     var self = this;
   1556 
   1557     var ui = $.summernote.ui;
   1558     this.memos = {};
   1559     this.modules = {};
   1560     this.layoutInfo = {};
   1561     this.options = options;
   1562 
   1563     /**
   1564      * create layout and initialize modules and other resources
   1565      */
   1566     this.initialize = function () {
   1567       this.layoutInfo = ui.createLayout($note, options);
   1568       this._initialize();
   1569       $note.hide();
   1570       return this;
   1571     };
   1572 
   1573     /**
   1574      * destroy modules and other resources and remove layout
   1575      */
   1576     this.destroy = function () {
   1577       this._destroy();
   1578       $note.removeData('summernote');
   1579       ui.removeLayout($note, this.layoutInfo);
   1580     };
   1581 
   1582     /**
   1583      * destory modules and other resources and initialize it again
   1584      */
   1585     this.reset = function () {
   1586       var disabled = self.isDisabled();
   1587       this.code(dom.emptyPara);
   1588       this._destroy();
   1589       this._initialize();
   1590 
   1591       if (disabled) {
   1592         self.disable();
   1593       }
   1594     };
   1595 
   1596     this._initialize = function () {
   1597       // add optional buttons
   1598       var buttons = $.extend({}, this.options.buttons);
   1599       Object.keys(buttons).forEach(function (key) {
   1600         self.memo('button.' + key, buttons[key]);
   1601       });
   1602 
   1603       var modules = $.extend({}, this.options.modules, $.summernote.plugins || {});
   1604 
   1605       // add and initialize modules
   1606       Object.keys(modules).forEach(function (key) {
   1607         self.module(key, modules[key], true);
   1608       });
   1609 
   1610       Object.keys(this.modules).forEach(function (key) {
   1611         self.initializeModule(key);
   1612       });
   1613     };
   1614 
   1615     this._destroy = function () {
   1616       // destroy modules with reversed order
   1617       Object.keys(this.modules).reverse().forEach(function (key) {
   1618         self.removeModule(key);
   1619       });
   1620 
   1621       Object.keys(this.memos).forEach(function (key) {
   1622         self.removeMemo(key);
   1623       });
   1624     };
   1625 
   1626     this.code = function (html) {
   1627       var isActivated = this.invoke('codeview.isActivated');
   1628 
   1629       if (html === undefined) {
   1630         this.invoke('codeview.sync');
   1631         return isActivated ? this.layoutInfo.codable.val() : this.layoutInfo.editable.html();
   1632       } else {
   1633         if (isActivated) {
   1634           this.layoutInfo.codable.val(html);
   1635         } else {
   1636           this.layoutInfo.editable.html(html);
   1637         }
   1638         $note.val(html);
   1639         this.triggerEvent('change', html);
   1640       }
   1641     };
   1642 
   1643     this.isDisabled = function () {
   1644       return this.layoutInfo.editable.attr('contenteditable') === 'false';
   1645     };
   1646 
   1647     this.enable = function () {
   1648       this.layoutInfo.editable.attr('contenteditable', true);
   1649       this.invoke('toolbar.activate', true);
   1650     };
   1651 
   1652     this.disable = function () {
   1653       // close codeview if codeview is opend
   1654       if (this.invoke('codeview.isActivated')) {
   1655         this.invoke('codeview.deactivate');
   1656       }
   1657       this.layoutInfo.editable.attr('contenteditable', false);
   1658       this.invoke('toolbar.deactivate', true);
   1659     };
   1660 
   1661     this.triggerEvent = function () {
   1662       var namespace = list.head(arguments);
   1663       var args = list.tail(list.from(arguments));
   1664 
   1665       var callback = this.options.callbacks[func.namespaceToCamel(namespace, 'on')];
   1666       if (callback) {
   1667         callback.apply($note[0], args);
   1668       }
   1669       $note.trigger('summernote.' + namespace, args);
   1670     };
   1671 
   1672     this.initializeModule = function (key) {
   1673       var module = this.modules[key];
   1674       module.shouldInitialize = module.shouldInitialize || func.ok;
   1675       if (!module.shouldInitialize()) {
   1676         return;
   1677       }
   1678 
   1679       // initialize module
   1680       if (module.initialize) {
   1681         module.initialize();
   1682       }
   1683 
   1684       // attach events
   1685       if (module.events) {
   1686         dom.attachEvents($note, module.events);
   1687       }
   1688     };
   1689 
   1690     this.module = function (key, ModuleClass, withoutIntialize) {
   1691       if (arguments.length === 1) {
   1692         return this.modules[key];
   1693       }
   1694 
   1695       this.modules[key] = new ModuleClass(this);
   1696 
   1697       if (!withoutIntialize) {
   1698         this.initializeModule(key);
   1699       }
   1700     };
   1701 
   1702     this.removeModule = function (key) {
   1703       var module = this.modules[key];
   1704       if (module.shouldInitialize()) {
   1705         if (module.events) {
   1706           dom.detachEvents($note, module.events);
   1707         }
   1708 
   1709         if (module.destroy) {
   1710           module.destroy();
   1711         }
   1712       }
   1713 
   1714       delete this.modules[key];
   1715     };
   1716 
   1717     this.memo = function (key, obj) {
   1718       if (arguments.length === 1) {
   1719         return this.memos[key];
   1720       }
   1721       this.memos[key] = obj;
   1722     };
   1723 
   1724     this.removeMemo = function (key) {
   1725       if (this.memos[key] && this.memos[key].destroy) {
   1726         this.memos[key].destroy();
   1727       }
   1728 
   1729       delete this.memos[key];
   1730     };
   1731 
   1732     this.createInvokeHandler = function (namespace, value) {
   1733       return function (event) {
   1734         event.preventDefault();
   1735         self.invoke(namespace, value || $(event.target).closest('[data-value]').data('value'));
   1736       };
   1737     };
   1738 
   1739     this.invoke = function () {
   1740       var namespace = list.head(arguments);
   1741       var args = list.tail(list.from(arguments));
   1742 
   1743       var splits = namespace.split('.');
   1744       var hasSeparator = splits.length > 1;
   1745       var moduleName = hasSeparator && list.head(splits);
   1746       var methodName = hasSeparator ? list.last(splits) : list.head(splits);
   1747 
   1748       var module = this.modules[moduleName || 'editor'];
   1749       if (!moduleName && this[methodName]) {
   1750         return this[methodName].apply(this, args);
   1751       } else if (module && module[methodName] && module.shouldInitialize()) {
   1752         return module[methodName].apply(module, args);
   1753       }
   1754     };
   1755 
   1756     return this.initialize();
   1757   };
   1758 
   1759   $.fn.extend({
   1760     /**
   1761      * Summernote API
   1762      *
   1763      * @param {Object|String}
   1764      * @return {this}
   1765      */
   1766     summernote: function () {
   1767       var type = $.type(list.head(arguments));
   1768       var isExternalAPICalled = type === 'string';
   1769       var hasInitOptions = type === 'object';
   1770 
   1771       var options = hasInitOptions ? list.head(arguments) : {};
   1772 
   1773       options = $.extend({}, $.summernote.options, options);
   1774       options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]);
   1775       options.icons = $.extend(true, {}, $.summernote.options.icons, options.icons);
   1776 
   1777       this.each(function (idx, note) {
   1778         var $note = $(note);
   1779         if (!$note.data('summernote')) {
   1780           var context = new Context($note, options);
   1781           $note.data('summernote', context);
   1782           $note.data('summernote').triggerEvent('init', context.layoutInfo);
   1783         }
   1784       });
   1785 
   1786       var $note = this.first();
   1787       if ($note.length) {
   1788         var context = $note.data('summernote');
   1789         if (isExternalAPICalled) {
   1790           return context.invoke.apply(context, list.from(arguments));
   1791         } else if (options.focus) {
   1792           context.invoke('editor.focus');
   1793         }
   1794       }
   1795 
   1796       return this;
   1797     }
   1798   });
   1799 
   1800 
   1801   var Renderer = function (markup, children, options, callback) {
   1802     this.render = function ($parent) {
   1803       var $node = $(markup);
   1804 
   1805       if (options && options.contents) {
   1806         $node.html(options.contents);
   1807       }
   1808 
   1809       if (options && options.className) {
   1810         $node.addClass(options.className);
   1811       }
   1812 
   1813       if (options && options.data) {
   1814         $.each(options.data, function (k, v) {
   1815           $node.attr('data-' + k, v);
   1816         });
   1817       }
   1818 
   1819       if (options && options.click) {
   1820         $node.on('click', options.click);
   1821       }
   1822 
   1823       if (children) {
   1824         var $container = $node.find('.note-children-container');
   1825         children.forEach(function (child) {
   1826           child.render($container.length ? $container : $node);
   1827         });
   1828       }
   1829 
   1830       if (callback) {
   1831         callback($node, options);
   1832       }
   1833 
   1834       if (options && options.callback) {
   1835         options.callback($node);
   1836       }
   1837 
   1838       if ($parent) {
   1839         $parent.append($node);
   1840       }
   1841 
   1842       return $node;
   1843     };
   1844   };
   1845 
   1846   var renderer = {
   1847     create: function (markup, callback) {
   1848       return function () {
   1849         var children = $.isArray(arguments[0]) ? arguments[0] : [];
   1850         var options = typeof arguments[1] === 'object' ? arguments[1] : arguments[0];
   1851         if (options && options.children) {
   1852           children = options.children;
   1853         }
   1854         return new Renderer(markup, children, options, callback);
   1855       };
   1856     }
   1857   };
   1858 
   1859   var editor = renderer.create('<div class="note-editor note-frame panel panel-default"/>');
   1860   var toolbar = renderer.create('<div class="note-toolbar panel-heading"/>');
   1861   var editingArea = renderer.create('<div class="note-editing-area"/>');
   1862   var codable = renderer.create('<textarea class="note-codable"/>');
   1863   var editable = renderer.create('<div class="note-editable panel-body" contentEditable="true"/>');
   1864   var statusbar = renderer.create([
   1865     '<div class="note-statusbar">',
   1866     '  <div class="note-resizebar">',
   1867     '    <div class="note-icon-bar"/>',
   1868     '    <div class="note-icon-bar"/>',
   1869     '    <div class="note-icon-bar"/>',
   1870     '  </div>',
   1871     '</div>'
   1872   ].join(''));
   1873 
   1874   var airEditor = renderer.create('<div class="note-editor"/>');
   1875   var airEditable = renderer.create('<div class="note-editable" contentEditable="true"/>');
   1876 
   1877   var buttonGroup = renderer.create('<div class="note-btn-group btn-group">');
   1878   var button = renderer.create('<button type="button" class="note-btn btn btn-default btn-sm" tabindex="-1">', function ($node, options) {
   1879     if (options && options.tooltip) {
   1880       $node.attr({
   1881         title: options.tooltip
   1882       }).tooltip({
   1883         container: 'body',
   1884         trigger: 'hover',
   1885         placement: 'bottom'
   1886       });
   1887     }
   1888   });
   1889 
   1890   var dropdown = renderer.create('<div class="dropdown-menu">', function ($node, options) {
   1891     var markup = $.isArray(options.items) ? options.items.map(function (item) {
   1892       var value = (typeof item === 'string') ? item : (item.value || '');
   1893       var content = options.template ? options.template(item) : item;
   1894       return '<li><a href="#" data-value="' + value + '">' + content + '</a></li>';
   1895     }).join('') : options.items;
   1896 
   1897     $node.html(markup);
   1898   });
   1899 
   1900   var dropdownCheck = renderer.create('<div class="dropdown-menu note-check">', function ($node, options) {
   1901     var markup = $.isArray(options.items) ? options.items.map(function (item) {
   1902       var value = (typeof item === 'string') ? item : (item.value || '');
   1903       var content = options.template ? options.template(item) : item;
   1904       return '<li><a href="#" data-value="' + value + '">' + icon(options.checkClassName) + ' ' + content + '</a></li>';
   1905     }).join('') : options.items;
   1906     $node.html(markup);
   1907   });
   1908 
   1909   var palette = renderer.create('<div class="note-color-palette"/>', function ($node, options) {
   1910     var contents = [];
   1911     for (var row = 0, rowSize = options.colors.length; row < rowSize; row++) {
   1912       var eventName = options.eventName;
   1913       var colors = options.colors[row];
   1914       var buttons = [];
   1915       for (var col = 0, colSize = colors.length; col < colSize; col++) {
   1916         var color = colors[col];
   1917         buttons.push([
   1918           '<button type="button" class="note-color-btn"',
   1919           'style="background-color:', color, '" ',
   1920           'data-event="', eventName, '" ',
   1921           'data-value="', color, '" ',
   1922           'title="', color, '" ',
   1923           'data-toggle="button" tabindex="-1"></button>'
   1924         ].join(''));
   1925       }
   1926       contents.push('<div class="note-color-row">' + buttons.join('') + '</div>');
   1927     }
   1928     $node.html(contents.join(''));
   1929 
   1930     $node.find('.note-color-btn').tooltip({
   1931       container: 'body',
   1932       trigger: 'hover',
   1933       placement: 'bottom'
   1934     });
   1935   });
   1936 
   1937   var dialog = renderer.create('<div class="modal" aria-hidden="false" tabindex="-1"/>', function ($node, options) {
   1938     if (options.fade) {
   1939       $node.addClass('fade');
   1940     }
   1941     $node.html([
   1942       '<div class="modal-dialog">',
   1943       '  <div class="modal-content">',
   1944       (options.title ?
   1945       '    <div class="modal-header">' +
   1946       '      <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>' +
   1947       '      <h4 class="modal-title">' + options.title + '</h4>' +
   1948       '    </div>' : ''
   1949       ),
   1950       '    <div class="modal-body">' + options.body + '</div>',
   1951       (options.footer ?
   1952       '    <div class="modal-footer">' + options.footer + '</div>' : ''
   1953       ),
   1954       '  </div>',
   1955       '</div>'
   1956     ].join(''));
   1957   });
   1958 
   1959   var popover = renderer.create([
   1960     '<div class="note-popover popover in">',
   1961     '  <div class="arrow"/>',
   1962     '  <div class="popover-content note-children-container"/>',
   1963     '</div>'
   1964   ].join(''), function ($node, options) {
   1965     var direction = typeof options.direction !== 'undefined' ? options.direction : 'bottom';
   1966 
   1967     $node.addClass(direction);
   1968 
   1969     if (options.hideArrow) {
   1970       $node.find('.arrow').hide();
   1971     }
   1972   });
   1973 
   1974   var icon = function (iconClassName, tagName) {
   1975     tagName = tagName || 'i';
   1976     return '<' + tagName + ' class="' + iconClassName + '"/>';
   1977   };
   1978 
   1979   var ui = {
   1980     editor: editor,
   1981     toolbar: toolbar,
   1982     editingArea: editingArea,
   1983     codable: codable,
   1984     editable: editable,
   1985     statusbar: statusbar,
   1986     airEditor: airEditor,
   1987     airEditable: airEditable,
   1988     buttonGroup: buttonGroup,
   1989     button: button,
   1990     dropdown: dropdown,
   1991     dropdownCheck: dropdownCheck,
   1992     palette: palette,
   1993     dialog: dialog,
   1994     popover: popover,
   1995     icon: icon,
   1996 
   1997     toggleBtn: function ($btn, isEnable) {
   1998       $btn.toggleClass('disabled', !isEnable);
   1999       $btn.attr('disabled', !isEnable);
   2000     },
   2001 
   2002     toggleBtnActive: function ($btn, isActive) {
   2003       $btn.toggleClass('active', isActive);
   2004     },
   2005 
   2006     onDialogShown: function ($dialog, handler) {
   2007       $dialog.one('shown.bs.modal', handler);
   2008     },
   2009 
   2010     onDialogHidden: function ($dialog, handler) {
   2011       $dialog.one('hidden.bs.modal', handler);
   2012     },
   2013 
   2014     showDialog: function ($dialog) {
   2015       $dialog.modal('show');
   2016     },
   2017 
   2018     hideDialog: function ($dialog) {
   2019       $dialog.modal('hide');
   2020     },
   2021 
   2022     createLayout: function ($note, options) {
   2023       var $editor = (options.airMode ? ui.airEditor([
   2024         ui.editingArea([
   2025           ui.airEditable()
   2026         ])
   2027       ]) : ui.editor([
   2028         ui.toolbar(),
   2029         ui.editingArea([
   2030           ui.codable(),
   2031           ui.editable()
   2032         ]),
   2033         ui.statusbar()
   2034       ])).render();
   2035 
   2036       $editor.insertAfter($note);
   2037 
   2038       return {
   2039         note: $note,
   2040         editor: $editor,
   2041         toolbar: $editor.find('.note-toolbar'),
   2042         editingArea: $editor.find('.note-editing-area'),
   2043         editable: $editor.find('.note-editable'),
   2044         codable: $editor.find('.note-codable'),
   2045         statusbar: $editor.find('.note-statusbar')
   2046       };
   2047     },
   2048 
   2049     removeLayout: function ($note, layoutInfo) {
   2050       $note.html(layoutInfo.editable.html());
   2051       layoutInfo.editor.remove();
   2052       $note.show();
   2053     }
   2054   };
   2055 
   2056   $.summernote = $.summernote || {
   2057     lang: {}
   2058   };
   2059 
   2060   $.extend($.summernote.lang, {
   2061     'en-US': {
   2062       font: {
   2063         bold: 'Bold',
   2064         italic: 'Italic',
   2065         underline: 'Underline',
   2066         clear: 'Remove Font Style',
   2067         height: 'Line Height',
   2068         name: 'Font Family',
   2069         strikethrough: 'Strikethrough',
   2070         subscript: 'Subscript',
   2071         superscript: 'Superscript',
   2072         size: 'Font Size'
   2073       },
   2074       image: {
   2075         image: 'Picture',
   2076         insert: 'Insert Image',
   2077         resizeFull: 'Resize Full',
   2078         resizeHalf: 'Resize Half',
   2079         resizeQuarter: 'Resize Quarter',
   2080         floatLeft: 'Float Left',
   2081         floatRight: 'Float Right',
   2082         floatNone: 'Float None',
   2083         shapeRounded: 'Shape: Rounded',
   2084         shapeCircle: 'Shape: Circle',
   2085         shapeThumbnail: 'Shape: Thumbnail',
   2086         shapeNone: 'Shape: None',
   2087         dragImageHere: 'Drag image or text here',
   2088         dropImage: 'Drop image or Text',
   2089         selectFromFiles: 'Select from files',
   2090         maximumFileSize: 'Maximum file size',
   2091         maximumFileSizeError: 'Maximum file size exceeded.',
   2092         url: 'Image URL',
   2093         remove: 'Remove Image'
   2094       },
   2095       video: {
   2096         video: 'Video',
   2097         videoLink: 'Video Link',
   2098         insert: 'Insert Video',
   2099         url: 'Video URL?',
   2100         providers: '(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)'
   2101       },
   2102       link: {
   2103         link: 'Link',
   2104         insert: 'Insert Link',
   2105         unlink: 'Unlink',
   2106         edit: 'Edit',
   2107         textToDisplay: 'Text to display',
   2108         url: 'To what URL should this link go?',
   2109         openInNewWindow: 'Open in new window'
   2110       },
   2111       table: {
   2112         table: 'Table'
   2113       },
   2114       hr: {
   2115         insert: 'Insert Horizontal Rule'
   2116       },
   2117       style: {
   2118         style: 'Style',
   2119         normal: 'Normal',
   2120         blockquote: 'Quote',
   2121         pre: 'Code',
   2122         h1: 'Header 1',
   2123         h2: 'Header 2',
   2124         h3: 'Header 3',
   2125         h4: 'Header 4',
   2126         h5: 'Header 5',
   2127         h6: 'Header 6'
   2128       },
   2129       lists: {
   2130         unordered: 'Unordered list',
   2131         ordered: 'Ordered list'
   2132       },
   2133       options: {
   2134         help: 'Help',
   2135         fullscreen: 'Full Screen',
   2136         codeview: 'Code View'
   2137       },
   2138       paragraph: {
   2139         paragraph: 'Paragraph',
   2140         outdent: 'Outdent',
   2141         indent: 'Indent',
   2142         left: 'Align left',
   2143         center: 'Align center',
   2144         right: 'Align right',
   2145         justify: 'Justify full'
   2146       },
   2147       color: {
   2148         recent: 'Recent Color',
   2149         more: 'More Color',
   2150         background: 'Background Color',
   2151         foreground: 'Foreground Color',
   2152         transparent: 'Transparent',
   2153         setTransparent: 'Set transparent',
   2154         reset: 'Reset',
   2155         resetToDefault: 'Reset to default'
   2156       },
   2157       shortcut: {
   2158         shortcuts: 'Keyboard shortcuts',
   2159         close: 'Close',
   2160         textFormatting: 'Text formatting',
   2161         action: 'Action',
   2162         paragraphFormatting: 'Paragraph formatting',
   2163         documentStyle: 'Document Style',
   2164         extraKeys: 'Extra keys'
   2165       },
   2166       help: {
   2167         'insertParagraph': 'Insert Paragraph',
   2168         'undo': 'Undoes the last command',
   2169         'redo': 'Redoes the last command',
   2170         'tab': 'Tab',
   2171         'untab': 'Untab',
   2172         'bold': 'Set a bold style',
   2173         'italic': 'Set a italic style',
   2174         'underline': 'Set a underline style',
   2175         'strikethrough': 'Set a strikethrough style',
   2176         'removeFormat': 'Clean a style',
   2177         'justifyLeft': 'Set left align',
   2178         'justifyCenter': 'Set center align',
   2179         'justifyRight': 'Set right align',
   2180         'justifyFull': 'Set full align',
   2181         'insertUnorderedList': 'Toggle unordered list',
   2182         'insertOrderedList': 'Toggle ordered list',
   2183         'outdent': 'Outdent on current paragraph',
   2184         'indent': 'Indent on current paragraph',
   2185         'formatPara': 'Change current block\'s format as a paragraph(P tag)',
   2186         'formatH1': 'Change current block\'s format as H1',
   2187         'formatH2': 'Change current block\'s format as H2',
   2188         'formatH3': 'Change current block\'s format as H3',
   2189         'formatH4': 'Change current block\'s format as H4',
   2190         'formatH5': 'Change current block\'s format as H5',
   2191         'formatH6': 'Change current block\'s format as H6',
   2192         'insertHorizontalRule': 'Insert horizontal rule',
   2193         'linkDialog.show': 'Show Link Dialog'
   2194       },
   2195       history: {
   2196         undo: 'Undo',
   2197         redo: 'Redo'
   2198       },
   2199       specialChar: {
   2200         specialChar: 'SPECIAL CHARACTERS',
   2201         select: 'Select Special characters'
   2202       }
   2203     }
   2204   });
   2205 
   2206 
   2207   /**
   2208    * @class core.key
   2209    *
   2210    * Object for keycodes.
   2211    *
   2212    * @singleton
   2213    * @alternateClassName key
   2214    */
   2215   var key = (function () {
   2216     var keyMap = {
   2217       'BACKSPACE': 8,
   2218       'TAB': 9,
   2219       'ENTER': 13,
   2220       'SPACE': 32,
   2221 
   2222       // Arrow
   2223       'LEFT': 37,
   2224       'UP': 38,
   2225       'RIGHT': 39,
   2226       'DOWN': 40,
   2227 
   2228       // Number: 0-9
   2229       'NUM0': 48,
   2230       'NUM1': 49,
   2231       'NUM2': 50,
   2232       'NUM3': 51,
   2233       'NUM4': 52,
   2234       'NUM5': 53,
   2235       'NUM6': 54,
   2236       'NUM7': 55,
   2237       'NUM8': 56,
   2238 
   2239       // Alphabet: a-z
   2240       'B': 66,
   2241       'E': 69,
   2242       'I': 73,
   2243       'J': 74,
   2244       'K': 75,
   2245       'L': 76,
   2246       'R': 82,
   2247       'S': 83,
   2248       'U': 85,
   2249       'V': 86,
   2250       'Y': 89,
   2251       'Z': 90,
   2252 
   2253       'SLASH': 191,
   2254       'LEFTBRACKET': 219,
   2255       'BACKSLASH': 220,
   2256       'RIGHTBRACKET': 221
   2257     };
   2258 
   2259     return {
   2260       /**
   2261        * @method isEdit
   2262        *
   2263        * @param {Number} keyCode
   2264        * @return {Boolean}
   2265        */
   2266       isEdit: function (keyCode) {
   2267         return list.contains([
   2268           keyMap.BACKSPACE,
   2269           keyMap.TAB,
   2270           keyMap.ENTER,
   2271           keyMap.SPACE
   2272         ], keyCode);
   2273       },
   2274       /**
   2275        * @method isMove
   2276        *
   2277        * @param {Number} keyCode
   2278        * @return {Boolean}
   2279        */
   2280       isMove: function (keyCode) {
   2281         return list.contains([
   2282           keyMap.LEFT,
   2283           keyMap.UP,
   2284           keyMap.RIGHT,
   2285           keyMap.DOWN
   2286         ], keyCode);
   2287       },
   2288       /**
   2289        * @property {Object} nameFromCode
   2290        * @property {String} nameFromCode.8 "BACKSPACE"
   2291        */
   2292       nameFromCode: func.invertObject(keyMap),
   2293       code: keyMap
   2294     };
   2295   })();
   2296 
   2297   var range = (function () {
   2298 
   2299     /**
   2300      * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
   2301      *
   2302      * @param {TextRange} textRange
   2303      * @param {Boolean} isStart
   2304      * @return {BoundaryPoint}
   2305      *
   2306      * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
   2307      */
   2308     var textRangeToPoint = function (textRange, isStart) {
   2309       var container = textRange.parentElement(), offset;
   2310   
   2311       var tester = document.body.createTextRange(), prevContainer;
   2312       var childNodes = list.from(container.childNodes);
   2313       for (offset = 0; offset < childNodes.length; offset++) {
   2314         if (dom.isText(childNodes[offset])) {
   2315           continue;
   2316         }
   2317         tester.moveToElementText(childNodes[offset]);
   2318         if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
   2319           break;
   2320         }
   2321         prevContainer = childNodes[offset];
   2322       }
   2323   
   2324       if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
   2325         var textRangeStart = document.body.createTextRange(), curTextNode = null;
   2326         textRangeStart.moveToElementText(prevContainer || container);
   2327         textRangeStart.collapse(!prevContainer);
   2328         curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
   2329   
   2330         var pointTester = textRange.duplicate();
   2331         pointTester.setEndPoint('StartToStart', textRangeStart);
   2332         var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
   2333   
   2334         while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
   2335           textCount -= curTextNode.nodeValue.length;
   2336           curTextNode = curTextNode.nextSibling;
   2337         }
   2338   
   2339         /* jshint ignore:start */
   2340         var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
   2341         /* jshint ignore:end */
   2342   
   2343         if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
   2344             textCount === curTextNode.nodeValue.length) {
   2345           textCount -= curTextNode.nodeValue.length;
   2346           curTextNode = curTextNode.nextSibling;
   2347         }
   2348   
   2349         container = curTextNode;
   2350         offset = textCount;
   2351       }
   2352   
   2353       return {
   2354         cont: container,
   2355         offset: offset
   2356       };
   2357     };
   2358     
   2359     /**
   2360      * return TextRange from boundary point (inspired by google closure-library)
   2361      * @param {BoundaryPoint} point
   2362      * @return {TextRange}
   2363      */
   2364     var pointToTextRange = function (point) {
   2365       var textRangeInfo = function (container, offset) {
   2366         var node, isCollapseToStart;
   2367   
   2368         if (dom.isText(container)) {
   2369           var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
   2370           var prevContainer = list.last(prevTextNodes).previousSibling;
   2371           node =  prevContainer || container.parentNode;
   2372           offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
   2373           isCollapseToStart = !prevContainer;
   2374         } else {
   2375           node = container.childNodes[offset] || container;
   2376           if (dom.isText(node)) {
   2377             return textRangeInfo(node, 0);
   2378           }
   2379   
   2380           offset = 0;
   2381           isCollapseToStart = false;
   2382         }
   2383   
   2384         return {
   2385           node: node,
   2386           collapseToStart: isCollapseToStart,
   2387           offset: offset
   2388         };
   2389       };
   2390   
   2391       var textRange = document.body.createTextRange();
   2392       var info = textRangeInfo(point.node, point.offset);
   2393   
   2394       textRange.moveToElementText(info.node);
   2395       textRange.collapse(info.collapseToStart);
   2396       textRange.moveStart('character', info.offset);
   2397       return textRange;
   2398     };
   2399     
   2400     /**
   2401      * Wrapped Range
   2402      *
   2403      * @constructor
   2404      * @param {Node} sc - start container
   2405      * @param {Number} so - start offset
   2406      * @param {Node} ec - end container
   2407      * @param {Number} eo - end offset
   2408      */
   2409     var WrappedRange = function (sc, so, ec, eo) {
   2410       this.sc = sc;
   2411       this.so = so;
   2412       this.ec = ec;
   2413       this.eo = eo;
   2414   
   2415       // nativeRange: get nativeRange from sc, so, ec, eo
   2416       var nativeRange = function () {
   2417         if (agent.isW3CRangeSupport) {
   2418           var w3cRange = document.createRange();
   2419           w3cRange.setStart(sc, so);
   2420           w3cRange.setEnd(ec, eo);
   2421 
   2422           return w3cRange;
   2423         } else {
   2424           var textRange = pointToTextRange({
   2425             node: sc,
   2426             offset: so
   2427           });
   2428 
   2429           textRange.setEndPoint('EndToEnd', pointToTextRange({
   2430             node: ec,
   2431             offset: eo
   2432           }));
   2433 
   2434           return textRange;
   2435         }
   2436       };
   2437 
   2438       this.getPoints = function () {
   2439         return {
   2440           sc: sc,
   2441           so: so,
   2442           ec: ec,
   2443           eo: eo
   2444         };
   2445       };
   2446 
   2447       this.getStartPoint = function () {
   2448         return {
   2449           node: sc,
   2450           offset: so
   2451         };
   2452       };
   2453 
   2454       this.getEndPoint = function () {
   2455         return {
   2456           node: ec,
   2457           offset: eo
   2458         };
   2459       };
   2460 
   2461       /**
   2462        * select update visible range
   2463        */
   2464       this.select = function () {
   2465         var nativeRng = nativeRange();
   2466         if (agent.isW3CRangeSupport) {
   2467           var selection = document.getSelection();
   2468           if (selection.rangeCount > 0) {
   2469             selection.removeAllRanges();
   2470           }
   2471           selection.addRange(nativeRng);
   2472         } else {
   2473           nativeRng.select();
   2474         }
   2475         
   2476         return this;
   2477       };
   2478 
   2479       /**
   2480        * Moves the scrollbar to start container(sc) of current range
   2481        *
   2482        * @return {WrappedRange}
   2483        */
   2484       this.scrollIntoView = function (container) {
   2485         var height = $(container).height();
   2486         if (container.scrollTop + height < this.sc.offsetTop) {
   2487           container.scrollTop += Math.abs(container.scrollTop + height - this.sc.offsetTop);
   2488         }
   2489 
   2490         return this;
   2491       };
   2492 
   2493       /**
   2494        * @return {WrappedRange}
   2495        */
   2496       this.normalize = function () {
   2497 
   2498         /**
   2499          * @param {BoundaryPoint} point
   2500          * @param {Boolean} isLeftToRight
   2501          * @return {BoundaryPoint}
   2502          */
   2503         var getVisiblePoint = function (point, isLeftToRight) {
   2504           if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
   2505               (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
   2506               (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
   2507               (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
   2508             return point;
   2509           }
   2510 
   2511           // point on block's edge
   2512           var block = dom.ancestor(point.node, dom.isBlock);
   2513           if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
   2514               ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
   2515 
   2516             // returns point already on visible point
   2517             if (dom.isVisiblePoint(point)) {
   2518               return point;
   2519             }
   2520             // reverse direction 
   2521             isLeftToRight = !isLeftToRight;
   2522           }
   2523 
   2524           var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
   2525                                           dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
   2526           return nextPoint || point;
   2527         };
   2528 
   2529         var endPoint = getVisiblePoint(this.getEndPoint(), false);
   2530         var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
   2531 
   2532         return new WrappedRange(
   2533           startPoint.node,
   2534           startPoint.offset,
   2535           endPoint.node,
   2536           endPoint.offset
   2537         );
   2538       };
   2539 
   2540       /**
   2541        * returns matched nodes on range
   2542        *
   2543        * @param {Function} [pred] - predicate function
   2544        * @param {Object} [options]
   2545        * @param {Boolean} [options.includeAncestor]
   2546        * @param {Boolean} [options.fullyContains]
   2547        * @return {Node[]}
   2548        */
   2549       this.nodes = function (pred, options) {
   2550         pred = pred || func.ok;
   2551 
   2552         var includeAncestor = options && options.includeAncestor;
   2553         var fullyContains = options && options.fullyContains;
   2554 
   2555         // TODO compare points and sort
   2556         var startPoint = this.getStartPoint();
   2557         var endPoint = this.getEndPoint();
   2558 
   2559         var nodes = [];
   2560         var leftEdgeNodes = [];
   2561 
   2562         dom.walkPoint(startPoint, endPoint, function (point) {
   2563           if (dom.isEditable(point.node)) {
   2564             return;
   2565           }
   2566 
   2567           var node;
   2568           if (fullyContains) {
   2569             if (dom.isLeftEdgePoint(point)) {
   2570               leftEdgeNodes.push(point.node);
   2571             }
   2572             if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
   2573               node = point.node;
   2574             }
   2575           } else if (includeAncestor) {
   2576             node = dom.ancestor(point.node, pred);
   2577           } else {
   2578             node = point.node;
   2579           }
   2580 
   2581           if (node && pred(node)) {
   2582             nodes.push(node);
   2583           }
   2584         }, true);
   2585 
   2586         return list.unique(nodes);
   2587       };
   2588 
   2589       /**
   2590        * returns commonAncestor of range
   2591        * @return {Element} - commonAncestor
   2592        */
   2593       this.commonAncestor = function () {
   2594         return dom.commonAncestor(sc, ec);
   2595       };
   2596 
   2597       /**
   2598        * returns expanded range by pred
   2599        *
   2600        * @param {Function} pred - predicate function
   2601        * @return {WrappedRange}
   2602        */
   2603       this.expand = function (pred) {
   2604         var startAncestor = dom.ancestor(sc, pred);
   2605         var endAncestor = dom.ancestor(ec, pred);
   2606 
   2607         if (!startAncestor && !endAncestor) {
   2608           return new WrappedRange(sc, so, ec, eo);
   2609         }
   2610 
   2611         var boundaryPoints = this.getPoints();
   2612 
   2613         if (startAncestor) {
   2614           boundaryPoints.sc = startAncestor;
   2615           boundaryPoints.so = 0;
   2616         }
   2617 
   2618         if (endAncestor) {
   2619           boundaryPoints.ec = endAncestor;
   2620           boundaryPoints.eo = dom.nodeLength(endAncestor);
   2621         }
   2622 
   2623         return new WrappedRange(
   2624           boundaryPoints.sc,
   2625           boundaryPoints.so,
   2626           boundaryPoints.ec,
   2627           boundaryPoints.eo
   2628         );
   2629       };
   2630 
   2631       /**
   2632        * @param {Boolean} isCollapseToStart
   2633        * @return {WrappedRange}
   2634        */
   2635       this.collapse = function (isCollapseToStart) {
   2636         if (isCollapseToStart) {
   2637           return new WrappedRange(sc, so, sc, so);
   2638         } else {
   2639           return new WrappedRange(ec, eo, ec, eo);
   2640         }
   2641       };
   2642 
   2643       /**
   2644        * splitText on range
   2645        */
   2646       this.splitText = function () {
   2647         var isSameContainer = sc === ec;
   2648         var boundaryPoints = this.getPoints();
   2649 
   2650         if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
   2651           ec.splitText(eo);
   2652         }
   2653 
   2654         if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
   2655           boundaryPoints.sc = sc.splitText(so);
   2656           boundaryPoints.so = 0;
   2657 
   2658           if (isSameContainer) {
   2659             boundaryPoints.ec = boundaryPoints.sc;
   2660             boundaryPoints.eo = eo - so;
   2661           }
   2662         }
   2663 
   2664         return new WrappedRange(
   2665           boundaryPoints.sc,
   2666           boundaryPoints.so,
   2667           boundaryPoints.ec,
   2668           boundaryPoints.eo
   2669         );
   2670       };
   2671 
   2672       /**
   2673        * delete contents on range
   2674        * @return {WrappedRange}
   2675        */
   2676       this.deleteContents = function () {
   2677         if (this.isCollapsed()) {
   2678           return this;
   2679         }
   2680 
   2681         var rng = this.splitText();
   2682         var nodes = rng.nodes(null, {
   2683           fullyContains: true
   2684         });
   2685 
   2686         // find new cursor point
   2687         var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
   2688           return !list.contains(nodes, point.node);
   2689         });
   2690 
   2691         var emptyParents = [];
   2692         $.each(nodes, function (idx, node) {
   2693           // find empty parents
   2694           var parent = node.parentNode;
   2695           if (point.node !== parent && dom.nodeLength(parent) === 1) {
   2696             emptyParents.push(parent);
   2697           }
   2698           dom.remove(node, false);
   2699         });
   2700 
   2701         // remove empty parents
   2702         $.each(emptyParents, function (idx, node) {
   2703           dom.remove(node, false);
   2704         });
   2705 
   2706         return new WrappedRange(
   2707           point.node,
   2708           point.offset,
   2709           point.node,
   2710           point.offset
   2711         ).normalize();
   2712       };
   2713       
   2714       /**
   2715        * makeIsOn: return isOn(pred) function
   2716        */
   2717       var makeIsOn = function (pred) {
   2718         return function () {
   2719           var ancestor = dom.ancestor(sc, pred);
   2720           return !!ancestor && (ancestor === dom.ancestor(ec, pred));
   2721         };
   2722       };
   2723   
   2724       // isOnEditable: judge whether range is on editable or not
   2725       this.isOnEditable = makeIsOn(dom.isEditable);
   2726       // isOnList: judge whether range is on list node or not
   2727       this.isOnList = makeIsOn(dom.isList);
   2728       // isOnAnchor: judge whether range is on anchor node or not
   2729       this.isOnAnchor = makeIsOn(dom.isAnchor);
   2730       // isOnCell: judge whether range is on cell node or not
   2731       this.isOnCell = makeIsOn(dom.isCell);
   2732       // isOnData: judge whether range is on data node or not
   2733       this.isOnData = makeIsOn(dom.isData);
   2734 
   2735       /**
   2736        * @param {Function} pred
   2737        * @return {Boolean}
   2738        */
   2739       this.isLeftEdgeOf = function (pred) {
   2740         if (!dom.isLeftEdgePoint(this.getStartPoint())) {
   2741           return false;
   2742         }
   2743 
   2744         var node = dom.ancestor(this.sc, pred);
   2745         return node && dom.isLeftEdgeOf(this.sc, node);
   2746       };
   2747 
   2748       /**
   2749        * returns whether range was collapsed or not
   2750        */
   2751       this.isCollapsed = function () {
   2752         return sc === ec && so === eo;
   2753       };
   2754 
   2755       /**
   2756        * wrap inline nodes which children of body with paragraph
   2757        *
   2758        * @return {WrappedRange}
   2759        */
   2760       this.wrapBodyInlineWithPara = function () {
   2761         if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
   2762           sc.innerHTML = dom.emptyPara;
   2763           return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
   2764         }
   2765 
   2766         /**
   2767          * [workaround] firefox often create range on not visible point. so normalize here.
   2768          *  - firefox: |<p>text</p>|
   2769          *  - chrome: <p>|text|</p>
   2770          */
   2771         var rng = this.normalize();
   2772         if (dom.isParaInline(sc) || dom.isPara(sc)) {
   2773           return rng;
   2774         }
   2775 
   2776         // find inline top ancestor
   2777         var topAncestor;
   2778         if (dom.isInline(rng.sc)) {
   2779           var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
   2780           topAncestor = list.last(ancestors);
   2781           if (!dom.isInline(topAncestor)) {
   2782             topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
   2783           }
   2784         } else {
   2785           topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
   2786         }
   2787 
   2788         // siblings not in paragraph
   2789         var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
   2790         inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
   2791 
   2792         // wrap with paragraph
   2793         if (inlineSiblings.length) {
   2794           var para = dom.wrap(list.head(inlineSiblings), 'p');
   2795           dom.appendChildNodes(para, list.tail(inlineSiblings));
   2796         }
   2797 
   2798         return this.normalize();
   2799       };
   2800 
   2801       /**
   2802        * insert node at current cursor
   2803        *
   2804        * @param {Node} node
   2805        * @return {Node}
   2806        */
   2807       this.insertNode = function (node) {
   2808         var rng = this.wrapBodyInlineWithPara().deleteContents();
   2809         var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
   2810 
   2811         if (info.rightNode) {
   2812           info.rightNode.parentNode.insertBefore(node, info.rightNode);
   2813         } else {
   2814           info.container.appendChild(node);
   2815         }
   2816 
   2817         return node;
   2818       };
   2819 
   2820       /**
   2821        * insert html at current cursor
   2822        */
   2823       this.pasteHTML = function (markup) {
   2824         var contentsContainer = $('<div></div>').html(markup)[0];
   2825         var childNodes = list.from(contentsContainer.childNodes);
   2826 
   2827         var rng = this.wrapBodyInlineWithPara().deleteContents();
   2828 
   2829         return childNodes.reverse().map(function (childNode) {
   2830           return rng.insertNode(childNode);
   2831         }).reverse();
   2832       };
   2833   
   2834       /**
   2835        * returns text in range
   2836        *
   2837        * @return {String}
   2838        */
   2839       this.toString = function () {
   2840         var nativeRng = nativeRange();
   2841         return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
   2842       };
   2843 
   2844       /**
   2845        * returns range for word before cursor
   2846        *
   2847        * @param {Boolean} [findAfter] - find after cursor, default: false
   2848        * @return {WrappedRange}
   2849        */
   2850       this.getWordRange = function (findAfter) {
   2851         var endPoint = this.getEndPoint();
   2852 
   2853         if (!dom.isCharPoint(endPoint)) {
   2854           return this;
   2855         }
   2856 
   2857         var startPoint = dom.prevPointUntil(endPoint, function (point) {
   2858           return !dom.isCharPoint(point);
   2859         });
   2860 
   2861         if (findAfter) {
   2862           endPoint = dom.nextPointUntil(endPoint, function (point) {
   2863             return !dom.isCharPoint(point);
   2864           });
   2865         }
   2866 
   2867         return new WrappedRange(
   2868           startPoint.node,
   2869           startPoint.offset,
   2870           endPoint.node,
   2871           endPoint.offset
   2872         );
   2873       };
   2874   
   2875       /**
   2876        * create offsetPath bookmark
   2877        *
   2878        * @param {Node} editable
   2879        */
   2880       this.bookmark = function (editable) {
   2881         return {
   2882           s: {
   2883             path: dom.makeOffsetPath(editable, sc),
   2884             offset: so
   2885           },
   2886           e: {
   2887             path: dom.makeOffsetPath(editable, ec),
   2888             offset: eo
   2889           }
   2890         };
   2891       };
   2892 
   2893       /**
   2894        * create offsetPath bookmark base on paragraph
   2895        *
   2896        * @param {Node[]} paras
   2897        */
   2898       this.paraBookmark = function (paras) {
   2899         return {
   2900           s: {
   2901             path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
   2902             offset: so
   2903           },
   2904           e: {
   2905             path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
   2906             offset: eo
   2907           }
   2908         };
   2909       };
   2910 
   2911       /**
   2912        * getClientRects
   2913        * @return {Rect[]}
   2914        */
   2915       this.getClientRects = function () {
   2916         var nativeRng = nativeRange();
   2917         return nativeRng.getClientRects();
   2918       };
   2919     };
   2920 
   2921   /**
   2922    * @class core.range
   2923    *
   2924    * Data structure
   2925    *  * BoundaryPoint: a point of dom tree
   2926    *  * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range
   2927    *
   2928    * See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
   2929    *
   2930    * @singleton
   2931    * @alternateClassName range
   2932    */
   2933     return {
   2934       /**
   2935        * create Range Object From arguments or Browser Selection
   2936        *
   2937        * @param {Node} sc - start container
   2938        * @param {Number} so - start offset
   2939        * @param {Node} ec - end container
   2940        * @param {Number} eo - end offset
   2941        * @return {WrappedRange}
   2942        */
   2943       create: function (sc, so, ec, eo) {
   2944         if (arguments.length === 4) {
   2945           return new WrappedRange(sc, so, ec, eo);
   2946         } else if (arguments.length === 2) { //collapsed
   2947           ec = sc;
   2948           eo = so;
   2949           return new WrappedRange(sc, so, ec, eo);
   2950         } else {
   2951           var wrappedRange = this.createFromSelection();
   2952           if (!wrappedRange && arguments.length === 1) {
   2953             wrappedRange = this.createFromNode(arguments[0]);
   2954             return wrappedRange.collapse(dom.emptyPara === arguments[0].innerHTML);
   2955           }
   2956           return wrappedRange;
   2957         }
   2958       },
   2959 
   2960       createFromSelection: function () {
   2961         var sc, so, ec, eo;
   2962         if (agent.isW3CRangeSupport) {
   2963           var selection = document.getSelection();
   2964           if (!selection || selection.rangeCount === 0) {
   2965             return null;
   2966           } else if (dom.isBody(selection.anchorNode)) {
   2967             // Firefox: returns entire body as range on initialization.
   2968             // We won't never need it.
   2969             return null;
   2970           }
   2971 
   2972           var nativeRng = selection.getRangeAt(0);
   2973           sc = nativeRng.startContainer;
   2974           so = nativeRng.startOffset;
   2975           ec = nativeRng.endContainer;
   2976           eo = nativeRng.endOffset;
   2977         } else { // IE8: TextRange
   2978           var textRange = document.selection.createRange();
   2979           var textRangeEnd = textRange.duplicate();
   2980           textRangeEnd.collapse(false);
   2981           var textRangeStart = textRange;
   2982           textRangeStart.collapse(true);
   2983 
   2984           var startPoint = textRangeToPoint(textRangeStart, true),
   2985           endPoint = textRangeToPoint(textRangeEnd, false);
   2986 
   2987           // same visible point case: range was collapsed.
   2988           if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
   2989               dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
   2990               endPoint.node.nextSibling === startPoint.node) {
   2991             startPoint = endPoint;
   2992           }
   2993 
   2994           sc = startPoint.cont;
   2995           so = startPoint.offset;
   2996           ec = endPoint.cont;
   2997           eo = endPoint.offset;
   2998         }
   2999 
   3000         return new WrappedRange(sc, so, ec, eo);
   3001       },
   3002 
   3003       /**
   3004        * @method 
   3005        * 
   3006        * create WrappedRange from node
   3007        *
   3008        * @param {Node} node
   3009        * @return {WrappedRange}
   3010        */
   3011       createFromNode: function (node) {
   3012         var sc = node;
   3013         var so = 0;
   3014         var ec = node;
   3015         var eo = dom.nodeLength(ec);
   3016 
   3017         // browsers can't target a picture or void node
   3018         if (dom.isVoid(sc)) {
   3019           so = dom.listPrev(sc).length - 1;
   3020           sc = sc.parentNode;
   3021         }
   3022         if (dom.isBR(ec)) {
   3023           eo = dom.listPrev(ec).length - 1;
   3024           ec = ec.parentNode;
   3025         } else if (dom.isVoid(ec)) {
   3026           eo = dom.listPrev(ec).length;
   3027           ec = ec.parentNode;
   3028         }
   3029 
   3030         return this.create(sc, so, ec, eo);
   3031       },
   3032 
   3033       /**
   3034        * create WrappedRange from node after position
   3035        *
   3036        * @param {Node} node
   3037        * @return {WrappedRange}
   3038        */
   3039       createFromNodeBefore: function (node) {
   3040         return this.createFromNode(node).collapse(true);
   3041       },
   3042 
   3043       /**
   3044        * create WrappedRange from node after position
   3045        *
   3046        * @param {Node} node
   3047        * @return {WrappedRange}
   3048        */
   3049       createFromNodeAfter: function (node) {
   3050         return this.createFromNode(node).collapse();
   3051       },
   3052 
   3053       /**
   3054        * @method 
   3055        * 
   3056        * create WrappedRange from bookmark
   3057        *
   3058        * @param {Node} editable
   3059        * @param {Object} bookmark
   3060        * @return {WrappedRange}
   3061        */
   3062       createFromBookmark: function (editable, bookmark) {
   3063         var sc = dom.fromOffsetPath(editable, bookmark.s.path);
   3064         var so = bookmark.s.offset;
   3065         var ec = dom.fromOffsetPath(editable, bookmark.e.path);
   3066         var eo = bookmark.e.offset;
   3067         return new WrappedRange(sc, so, ec, eo);
   3068       },
   3069 
   3070       /**
   3071        * @method 
   3072        *
   3073        * create WrappedRange from paraBookmark
   3074        *
   3075        * @param {Object} bookmark
   3076        * @param {Node[]} paras
   3077        * @return {WrappedRange}
   3078        */
   3079       createFromParaBookmark: function (bookmark, paras) {
   3080         var so = bookmark.s.offset;
   3081         var eo = bookmark.e.offset;
   3082         var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path);
   3083         var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path);
   3084 
   3085         return new WrappedRange(sc, so, ec, eo);
   3086       }
   3087     };
   3088   })();
   3089 
   3090   /**
   3091    * @class core.async
   3092    *
   3093    * Async functions which returns `Promise`
   3094    *
   3095    * @singleton
   3096    * @alternateClassName async
   3097    */
   3098   var async = (function () {
   3099     /**
   3100      * @method readFileAsDataURL
   3101      *
   3102      * read contents of file as representing URL
   3103      *
   3104      * @param {File} file
   3105      * @return {Promise} - then: dataUrl
   3106      */
   3107     var readFileAsDataURL = function (file) {
   3108       return $.Deferred(function (deferred) {
   3109         $.extend(new FileReader(), {
   3110           onload: function (e) {
   3111             var dataURL = e.target.result;
   3112             deferred.resolve(dataURL);
   3113           },
   3114           onerror: function () {
   3115             deferred.reject(this);
   3116           }
   3117         }).readAsDataURL(file);
   3118       }).promise();
   3119     };
   3120   
   3121     /**
   3122      * @method createImage
   3123      *
   3124      * create `<image>` from url string
   3125      *
   3126      * @param {String} url
   3127      * @return {Promise} - then: $image
   3128      */
   3129     var createImage = function (url) {
   3130       return $.Deferred(function (deferred) {
   3131         var $img = $('<img>');
   3132 
   3133         $img.one('load', function () {
   3134           $img.off('error abort');
   3135           deferred.resolve($img);
   3136         }).one('error abort', function () {
   3137           $img.off('load').detach();
   3138           deferred.reject($img);
   3139         }).css({
   3140           display: 'none'
   3141         }).appendTo(document.body).attr('src', url);
   3142       }).promise();
   3143     };
   3144 
   3145     return {
   3146       readFileAsDataURL: readFileAsDataURL,
   3147       createImage: createImage
   3148     };
   3149   })();
   3150 
   3151   /**
   3152    * @class editing.History
   3153    *
   3154    * Editor History
   3155    *
   3156    */
   3157   var History = function ($editable) {
   3158     var stack = [], stackOffset = -1;
   3159     var editable = $editable[0];
   3160 
   3161     var makeSnapshot = function () {
   3162       var rng = range.create(editable);
   3163       var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}};
   3164 
   3165       return {
   3166         contents: $editable.html(),
   3167         bookmark: (rng ? rng.bookmark(editable) : emptyBookmark)
   3168       };
   3169     };
   3170 
   3171     var applySnapshot = function (snapshot) {
   3172       if (snapshot.contents !== null) {
   3173         $editable.html(snapshot.contents);
   3174       }
   3175       if (snapshot.bookmark !== null) {
   3176         range.createFromBookmark(editable, snapshot.bookmark).select();
   3177       }
   3178     };
   3179 
   3180     /**
   3181     * @method rewind
   3182     * Rewinds the history stack back to the first snapshot taken.
   3183     * Leaves the stack intact, so that "Redo" can still be used.
   3184     */
   3185     this.rewind = function () {
   3186       // Create snap shot if not yet recorded
   3187       if ($editable.html() !== stack[stackOffset].contents) {
   3188         this.recordUndo();
   3189       }
   3190 
   3191       // Return to the first available snapshot.
   3192       stackOffset = 0;
   3193 
   3194       // Apply that snapshot.
   3195       applySnapshot(stack[stackOffset]);
   3196     };
   3197 
   3198     /**
   3199     * @method reset
   3200     * Resets the history stack completely; reverting to an empty editor.
   3201     */
   3202     this.reset = function () {
   3203       // Clear the stack.
   3204       stack = [];
   3205 
   3206       // Restore stackOffset to its original value.
   3207       stackOffset = -1;
   3208 
   3209       // Clear the editable area.
   3210       $editable.html('');
   3211 
   3212       // Record our first snapshot (of nothing).
   3213       this.recordUndo();
   3214     };
   3215 
   3216     /**
   3217      * undo
   3218      */
   3219     this.undo = function () {
   3220       // Create snap shot if not yet recorded
   3221       if ($editable.html() !== stack[stackOffset].contents) {
   3222         this.recordUndo();
   3223       }
   3224 
   3225       if (0 < stackOffset) {
   3226         stackOffset--;
   3227         applySnapshot(stack[stackOffset]);
   3228       }
   3229     };
   3230 
   3231     /**
   3232      * redo
   3233      */
   3234     this.redo = function () {
   3235       if (stack.length - 1 > stackOffset) {
   3236         stackOffset++;
   3237         applySnapshot(stack[stackOffset]);
   3238       }
   3239     };
   3240 
   3241     /**
   3242      * recorded undo
   3243      */
   3244     this.recordUndo = function () {
   3245       stackOffset++;
   3246 
   3247       // Wash out stack after stackOffset
   3248       if (stack.length > stackOffset) {
   3249         stack = stack.slice(0, stackOffset);
   3250       }
   3251 
   3252       // Create new snapshot and push it to the end
   3253       stack.push(makeSnapshot());
   3254     };
   3255   };
   3256 
   3257   /**
   3258    * @class editing.Style
   3259    *
   3260    * Style
   3261    *
   3262    */
   3263   var Style = function () {
   3264     /**
   3265      * @method jQueryCSS
   3266      *
   3267      * [workaround] for old jQuery
   3268      * passing an array of style properties to .css()
   3269      * will result in an object of property-value pairs.
   3270      * (compability with version < 1.9)
   3271      *
   3272      * @private
   3273      * @param  {jQuery} $obj
   3274      * @param  {Array} propertyNames - An array of one or more CSS properties.
   3275      * @return {Object}
   3276      */
   3277     var jQueryCSS = function ($obj, propertyNames) {
   3278       if (agent.jqueryVersion < 1.9) {
   3279         var result = {};
   3280         $.each(propertyNames, function (idx, propertyName) {
   3281           result[propertyName] = $obj.css(propertyName);
   3282         });
   3283         return result;
   3284       }
   3285       return $obj.css.call($obj, propertyNames);
   3286     };
   3287 
   3288     /**
   3289      * returns style object from node
   3290      *
   3291      * @param {jQuery} $node
   3292      * @return {Object}
   3293      */
   3294     this.fromNode = function ($node) {
   3295       var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height'];
   3296       var styleInfo = jQueryCSS($node, properties) || {};
   3297       styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10);
   3298       return styleInfo;
   3299     };
   3300 
   3301     /**
   3302      * paragraph level style
   3303      *
   3304      * @param {WrappedRange} rng
   3305      * @param {Object} styleInfo
   3306      */
   3307     this.stylePara = function (rng, styleInfo) {
   3308       $.each(rng.nodes(dom.isPara, {
   3309         includeAncestor: true
   3310       }), function (idx, para) {
   3311         $(para).css(styleInfo);
   3312       });
   3313     };
   3314 
   3315     /**
   3316      * insert and returns styleNodes on range.
   3317      *
   3318      * @param {WrappedRange} rng
   3319      * @param {Object} [options] - options for styleNodes
   3320      * @param {String} [options.nodeName] - default: `SPAN`
   3321      * @param {Boolean} [options.expandClosestSibling] - default: `false`
   3322      * @param {Boolean} [options.onlyPartialContains] - default: `false`
   3323      * @return {Node[]}
   3324      */
   3325     this.styleNodes = function (rng, options) {
   3326       rng = rng.splitText();
   3327 
   3328       var nodeName = options && options.nodeName || 'SPAN';
   3329       var expandClosestSibling = !!(options && options.expandClosestSibling);
   3330       var onlyPartialContains = !!(options && options.onlyPartialContains);
   3331 
   3332       if (rng.isCollapsed()) {
   3333         return [rng.insertNode(dom.create(nodeName))];
   3334       }
   3335 
   3336       var pred = dom.makePredByNodeName(nodeName);
   3337       var nodes = rng.nodes(dom.isText, {
   3338         fullyContains: true
   3339       }).map(function (text) {
   3340         return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName);
   3341       });
   3342 
   3343       if (expandClosestSibling) {
   3344         if (onlyPartialContains) {
   3345           var nodesInRange = rng.nodes();
   3346           // compose with partial contains predication
   3347           pred = func.and(pred, function (node) {
   3348             return list.contains(nodesInRange, node);
   3349           });
   3350         }
   3351 
   3352         return nodes.map(function (node) {
   3353           var siblings = dom.withClosestSiblings(node, pred);
   3354           var head = list.head(siblings);
   3355           var tails = list.tail(siblings);
   3356           $.each(tails, function (idx, elem) {
   3357             dom.appendChildNodes(head, elem.childNodes);
   3358             dom.remove(elem);
   3359           });
   3360           return list.head(siblings);
   3361         });
   3362       } else {
   3363         return nodes;
   3364       }
   3365     };
   3366 
   3367     /**
   3368      * get current style on cursor
   3369      *
   3370      * @param {WrappedRange} rng
   3371      * @return {Object} - object contains style properties.
   3372      */
   3373     this.current = function (rng) {
   3374       var $cont = $(!dom.isElement(rng.sc) ? rng.sc.parentNode : rng.sc);
   3375       var styleInfo = this.fromNode($cont);
   3376 
   3377       // document.queryCommandState for toggle state
   3378       // [workaround] prevent Firefox nsresult: "0x80004005 (NS_ERROR_FAILURE)"
   3379       try {
   3380         styleInfo = $.extend(styleInfo, {
   3381           'font-bold': document.queryCommandState('bold') ? 'bold' : 'normal',
   3382           'font-italic': document.queryCommandState('italic') ? 'italic' : 'normal',
   3383           'font-underline': document.queryCommandState('underline') ? 'underline' : 'normal',
   3384           'font-subscript': document.queryCommandState('subscript') ? 'subscript' : 'normal',
   3385           'font-superscript': document.queryCommandState('superscript') ? 'superscript' : 'normal',
   3386           'font-strikethrough': document.queryCommandState('strikethrough') ? 'strikethrough' : 'normal'
   3387         });
   3388       } catch (e) {}
   3389 
   3390       // list-style-type to list-style(unordered, ordered)
   3391       if (!rng.isOnList()) {
   3392         styleInfo['list-style'] = 'none';
   3393       } else {
   3394         var orderedTypes = ['circle', 'disc', 'disc-leading-zero', 'square'];
   3395         var isUnordered = $.inArray(styleInfo['list-style-type'], orderedTypes) > -1;
   3396         styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered';
   3397       }
   3398 
   3399       var para = dom.ancestor(rng.sc, dom.isPara);
   3400       if (para && para.style['line-height']) {
   3401         styleInfo['line-height'] = para.style.lineHeight;
   3402       } else {
   3403         var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10);
   3404         styleInfo['line-height'] = lineHeight.toFixed(1);
   3405       }
   3406 
   3407       styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor);
   3408       styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable);
   3409       styleInfo.range = rng;
   3410 
   3411       return styleInfo;
   3412     };
   3413   };
   3414 
   3415 
   3416   /**
   3417    * @class editing.Bullet
   3418    *
   3419    * @alternateClassName Bullet
   3420    */
   3421   var Bullet = function () {
   3422     var self = this;
   3423 
   3424     /**
   3425      * toggle ordered list
   3426      */
   3427     this.insertOrderedList = function (editable) {
   3428       this.toggleList('OL', editable);
   3429     };
   3430 
   3431     /**
   3432      * toggle unordered list
   3433      */
   3434     this.insertUnorderedList = function (editable) {
   3435       this.toggleList('UL', editable);
   3436     };
   3437 
   3438     /**
   3439      * indent
   3440      */
   3441     this.indent = function (editable) {
   3442       var self = this;
   3443       var rng = range.create(editable).wrapBodyInlineWithPara();
   3444 
   3445       var paras = rng.nodes(dom.isPara, { includeAncestor: true });
   3446       var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
   3447 
   3448       $.each(clustereds, function (idx, paras) {
   3449         var head = list.head(paras);
   3450         if (dom.isLi(head)) {
   3451           self.wrapList(paras, head.parentNode.nodeName);
   3452         } else {
   3453           $.each(paras, function (idx, para) {
   3454             $(para).css('marginLeft', function (idx, val) {
   3455               return (parseInt(val, 10) || 0) + 25;
   3456             });
   3457           });
   3458         }
   3459       });
   3460 
   3461       rng.select();
   3462     };
   3463 
   3464     /**
   3465      * outdent
   3466      */
   3467     this.outdent = function (editable) {
   3468       var self = this;
   3469       var rng = range.create(editable).wrapBodyInlineWithPara();
   3470 
   3471       var paras = rng.nodes(dom.isPara, { includeAncestor: true });
   3472       var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
   3473 
   3474       $.each(clustereds, function (idx, paras) {
   3475         var head = list.head(paras);
   3476         if (dom.isLi(head)) {
   3477           self.releaseList([paras]);
   3478         } else {
   3479           $.each(paras, function (idx, para) {
   3480             $(para).css('marginLeft', function (idx, val) {
   3481               val = (parseInt(val, 10) || 0);
   3482               return val > 25 ? val - 25 : '';
   3483             });
   3484           });
   3485         }
   3486       });
   3487 
   3488       rng.select();
   3489     };
   3490 
   3491     /**
   3492      * toggle list
   3493      *
   3494      * @param {String} listName - OL or UL
   3495      */
   3496     this.toggleList = function (listName, editable) {
   3497       var rng = range.create(editable).wrapBodyInlineWithPara();
   3498 
   3499       var paras = rng.nodes(dom.isPara, { includeAncestor: true });
   3500       var bookmark = rng.paraBookmark(paras);
   3501       var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
   3502 
   3503       // paragraph to list
   3504       if (list.find(paras, dom.isPurePara)) {
   3505         var wrappedParas = [];
   3506         $.each(clustereds, function (idx, paras) {
   3507           wrappedParas = wrappedParas.concat(self.wrapList(paras, listName));
   3508         });
   3509         paras = wrappedParas;
   3510       // list to paragraph or change list style
   3511       } else {
   3512         var diffLists = rng.nodes(dom.isList, {
   3513           includeAncestor: true
   3514         }).filter(function (listNode) {
   3515           return !$.nodeName(listNode, listName);
   3516         });
   3517 
   3518         if (diffLists.length) {
   3519           $.each(diffLists, function (idx, listNode) {
   3520             dom.replace(listNode, listName);
   3521           });
   3522         } else {
   3523           paras = this.releaseList(clustereds, true);
   3524         }
   3525       }
   3526 
   3527       range.createFromParaBookmark(bookmark, paras).select();
   3528     };
   3529 
   3530     /**
   3531      * @param {Node[]} paras
   3532      * @param {String} listName
   3533      * @return {Node[]}
   3534      */
   3535     this.wrapList = function (paras, listName) {
   3536       var head = list.head(paras);
   3537       var last = list.last(paras);
   3538 
   3539       var prevList = dom.isList(head.previousSibling) && head.previousSibling;
   3540       var nextList = dom.isList(last.nextSibling) && last.nextSibling;
   3541 
   3542       var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last);
   3543 
   3544       // P to LI
   3545       paras = paras.map(function (para) {
   3546         return dom.isPurePara(para) ? dom.replace(para, 'LI') : para;
   3547       });
   3548 
   3549       // append to list(<ul>, <ol>)
   3550       dom.appendChildNodes(listNode, paras);
   3551 
   3552       if (nextList) {
   3553         dom.appendChildNodes(listNode, list.from(nextList.childNodes));
   3554         dom.remove(nextList);
   3555       }
   3556 
   3557       return paras;
   3558     };
   3559 
   3560     /**
   3561      * @method releaseList
   3562      *
   3563      * @param {Array[]} clustereds
   3564      * @param {Boolean} isEscapseToBody
   3565      * @return {Node[]}
   3566      */
   3567     this.releaseList = function (clustereds, isEscapseToBody) {
   3568       var releasedParas = [];
   3569 
   3570       $.each(clustereds, function (idx, paras) {
   3571         var head = list.head(paras);
   3572         var last = list.last(paras);
   3573 
   3574         var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) :
   3575                                          head.parentNode;
   3576         var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, {
   3577           node: last.parentNode,
   3578           offset: dom.position(last) + 1
   3579         }, {
   3580           isSkipPaddingBlankHTML: true
   3581         }) : null;
   3582 
   3583         var middleList = dom.splitTree(headList, {
   3584           node: head.parentNode,
   3585           offset: dom.position(head)
   3586         }, {
   3587           isSkipPaddingBlankHTML: true
   3588         });
   3589 
   3590         paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) :
   3591                                   list.from(middleList.childNodes).filter(dom.isLi);
   3592 
   3593         // LI to P
   3594         if (isEscapseToBody || !dom.isList(headList.parentNode)) {
   3595           paras = paras.map(function (para) {
   3596             return dom.replace(para, 'P');
   3597           });
   3598         }
   3599 
   3600         $.each(list.from(paras).reverse(), function (idx, para) {
   3601           dom.insertAfter(para, headList);
   3602         });
   3603 
   3604         // remove empty lists
   3605         var rootLists = list.compact([headList, middleList, lastList]);
   3606         $.each(rootLists, function (idx, rootList) {
   3607           var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList));
   3608           $.each(listNodes.reverse(), function (idx, listNode) {
   3609             if (!dom.nodeLength(listNode)) {
   3610               dom.remove(listNode, true);
   3611             }
   3612           });
   3613         });
   3614 
   3615         releasedParas = releasedParas.concat(paras);
   3616       });
   3617 
   3618       return releasedParas;
   3619     };
   3620   };
   3621 
   3622 
   3623   /**
   3624    * @class editing.Typing
   3625    *
   3626    * Typing
   3627    *
   3628    */
   3629   var Typing = function () {
   3630 
   3631     // a Bullet instance to toggle lists off
   3632     var bullet = new Bullet();
   3633 
   3634     /**
   3635      * insert tab
   3636      *
   3637      * @param {WrappedRange} rng
   3638      * @param {Number} tabsize
   3639      */
   3640     this.insertTab = function (rng, tabsize) {
   3641       var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR));
   3642       rng = rng.deleteContents();
   3643       rng.insertNode(tab, true);
   3644 
   3645       rng = range.create(tab, tabsize);
   3646       rng.select();
   3647     };
   3648 
   3649     /**
   3650      * insert paragraph
   3651      */
   3652     this.insertParagraph = function (editable) {
   3653       var rng = range.create(editable);
   3654 
   3655       // deleteContents on range.
   3656       rng = rng.deleteContents();
   3657 
   3658       // Wrap range if it needs to be wrapped by paragraph
   3659       rng = rng.wrapBodyInlineWithPara();
   3660 
   3661       // finding paragraph
   3662       var splitRoot = dom.ancestor(rng.sc, dom.isPara);
   3663 
   3664       var nextPara;
   3665       // on paragraph: split paragraph
   3666       if (splitRoot) {
   3667         // if it is an empty line with li
   3668         if (dom.isEmpty(splitRoot) && dom.isLi(splitRoot)) {
   3669           // toogle UL/OL and escape
   3670           bullet.toggleList(splitRoot.parentNode.nodeName);
   3671           return;
   3672         // if it is an empty line with para on blockquote
   3673         } else if (dom.isEmpty(splitRoot) && dom.isPara(splitRoot) && dom.isBlockquote(splitRoot.parentNode)) {
   3674           // escape blockquote
   3675           dom.insertAfter(splitRoot, splitRoot.parentNode);
   3676           nextPara = splitRoot;
   3677         // if new line has content (not a line break)
   3678         } else {
   3679           nextPara = dom.splitTree(splitRoot, rng.getStartPoint());
   3680 
   3681           var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor);
   3682           emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor));
   3683 
   3684           $.each(emptyAnchors, function (idx, anchor) {
   3685             dom.remove(anchor);
   3686           });
   3687 
   3688           // replace empty heading or pre with P tag
   3689           if ((dom.isHeading(nextPara) || dom.isPre(nextPara)) && dom.isEmpty(nextPara)) {
   3690             nextPara = dom.replace(nextPara, 'p');
   3691           }
   3692         }
   3693       // no paragraph: insert empty paragraph
   3694       } else {
   3695         var next = rng.sc.childNodes[rng.so];
   3696         nextPara = $(dom.emptyPara)[0];
   3697         if (next) {
   3698           rng.sc.insertBefore(nextPara, next);
   3699         } else {
   3700           rng.sc.appendChild(nextPara);
   3701         }
   3702       }
   3703 
   3704       range.create(nextPara, 0).normalize().select().scrollIntoView(editable);
   3705     };
   3706   };
   3707 
   3708   /**
   3709    * @class editing.Table
   3710    *
   3711    * Table
   3712    *
   3713    */
   3714   var Table = function () {
   3715     /**
   3716      * handle tab key
   3717      *
   3718      * @param {WrappedRange} rng
   3719      * @param {Boolean} isShift
   3720      */
   3721     this.tab = function (rng, isShift) {
   3722       var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
   3723       var table = dom.ancestor(cell, dom.isTable);
   3724       var cells = dom.listDescendant(table, dom.isCell);
   3725 
   3726       var nextCell = list[isShift ? 'prev' : 'next'](cells, cell);
   3727       if (nextCell) {
   3728         range.create(nextCell, 0).select();
   3729       }
   3730     };
   3731 
   3732     /**
   3733      * create empty table element
   3734      *
   3735      * @param {Number} rowCount
   3736      * @param {Number} colCount
   3737      * @return {Node}
   3738      */
   3739     this.createTable = function (colCount, rowCount, options) {
   3740       var tds = [], tdHTML;
   3741       for (var idxCol = 0; idxCol < colCount; idxCol++) {
   3742         tds.push('<td>' + dom.blank + '</td>');
   3743       }
   3744       tdHTML = tds.join('');
   3745 
   3746       var trs = [], trHTML;
   3747       for (var idxRow = 0; idxRow < rowCount; idxRow++) {
   3748         trs.push('<tr>' + tdHTML + '</tr>');
   3749       }
   3750       trHTML = trs.join('');
   3751       var $table = $('<table>' + trHTML + '</table>');
   3752       if (options && options.tableClassName) {
   3753         $table.addClass(options.tableClassName);
   3754       }
   3755 
   3756       return $table[0];
   3757     };
   3758   };
   3759 
   3760 
   3761   var KEY_BOGUS = 'bogus';
   3762 
   3763   /**
   3764    * @class Editor
   3765    */
   3766   var Editor = function (context) {
   3767     var self = this;
   3768 
   3769     var $note = context.layoutInfo.note;
   3770     var $editor = context.layoutInfo.editor;
   3771     var $editable = context.layoutInfo.editable;
   3772     var options = context.options;
   3773     var lang = options.langInfo;
   3774 
   3775     var editable = $editable[0];
   3776     var lastRange = null;
   3777 
   3778     var style = new Style();
   3779     var table = new Table();
   3780     var typing = new Typing();
   3781     var bullet = new Bullet();
   3782     var history = new History($editable);
   3783 
   3784     this.initialize = function () {
   3785       // bind custom events
   3786       $editable.on('keydown', function (event) {
   3787         if (event.keyCode === key.code.ENTER) {
   3788           context.triggerEvent('enter', event);
   3789         }
   3790         context.triggerEvent('keydown', event);
   3791 
   3792         if (!event.isDefaultPrevented()) {
   3793           if (options.shortcuts) {
   3794             self.handleKeyMap(event);
   3795           } else {
   3796             self.preventDefaultEditableShortCuts(event);
   3797           }
   3798         }
   3799       }).on('keyup', function (event) {
   3800         context.triggerEvent('keyup', event);
   3801       }).on('focus', function (event) {
   3802         context.triggerEvent('focus', event);
   3803       }).on('blur', function (event) {
   3804         context.triggerEvent('blur', event);
   3805       }).on('mousedown', function (event) {
   3806         context.triggerEvent('mousedown', event);
   3807       }).on('mouseup', function (event) {
   3808         context.triggerEvent('mouseup', event);
   3809       }).on('scroll', function (event) {
   3810         context.triggerEvent('scroll', event);
   3811       }).on('paste', function (event) {
   3812         context.triggerEvent('paste', event);
   3813       });
   3814 
   3815       // init content before set event
   3816       $editable.html(dom.html($note) || dom.emptyPara);
   3817 
   3818       // [workaround] IE doesn't have input events for contentEditable
   3819       // - see: https://goo.gl/4bfIvA
   3820       var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';
   3821       $editable.on(changeEventName, func.debounce(function () {
   3822         context.triggerEvent('change', $editable.html());
   3823       }, 250));
   3824 
   3825       $editor.on('focusin', function (event) {
   3826         context.triggerEvent('focusin', event);
   3827       }).on('focusout', function (event) {
   3828         context.triggerEvent('focusout', event);
   3829       });
   3830 
   3831       if (!options.airMode) {
   3832         if (options.width) {
   3833           $editor.outerWidth(options.width);
   3834         }
   3835         if (options.height) {
   3836           $editable.outerHeight(options.height);
   3837         }
   3838         if (options.maxHeight) {
   3839           $editable.css('max-height', options.maxHeight);
   3840         }
   3841         if (options.minHeight) {
   3842           $editable.css('min-height', options.minHeight);
   3843         }
   3844       }
   3845 
   3846       history.recordUndo();
   3847     };
   3848 
   3849     this.destroy = function () {
   3850       $editable.off();
   3851     };
   3852 
   3853     this.handleKeyMap = function (event) {
   3854       var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
   3855       var keys = [];
   3856 
   3857       if (event.metaKey) { keys.push('CMD'); }
   3858       if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); }
   3859       if (event.shiftKey) { keys.push('SHIFT'); }
   3860 
   3861       var keyName = key.nameFromCode[event.keyCode];
   3862       if (keyName) {
   3863         keys.push(keyName);
   3864       }
   3865 
   3866       var eventName = keyMap[keys.join('+')];
   3867       if (eventName) {
   3868         event.preventDefault();
   3869         context.invoke(eventName);
   3870       } else if (key.isEdit(event.keyCode)) {
   3871         this.afterCommand();
   3872       }
   3873     };
   3874 
   3875     this.preventDefaultEditableShortCuts = function (event) {
   3876       // B(Bold, 66) / I(Italic, 73) / U(Underline, 85)
   3877       if ((event.ctrlKey || event.metaKey) &&
   3878         list.contains([66, 73, 85], event.keyCode)) {
   3879         event.preventDefault();
   3880       }
   3881     };
   3882 
   3883     /**
   3884      * create range
   3885      * @return {WrappedRange}
   3886      */
   3887     this.createRange = function () {
   3888       this.focus();
   3889       return range.create(editable);
   3890     };
   3891 
   3892     /**
   3893      * saveRange
   3894      *
   3895      * save current range
   3896      *
   3897      * @param {Boolean} [thenCollapse=false]
   3898      */
   3899     this.saveRange = function (thenCollapse) {
   3900       lastRange = this.createRange();
   3901       if (thenCollapse) {
   3902         lastRange.collapse().select();
   3903       }
   3904     };
   3905 
   3906     /**
   3907      * restoreRange
   3908      *
   3909      * restore lately range
   3910      */
   3911     this.restoreRange = function () {
   3912       if (lastRange) {
   3913         lastRange.select();
   3914         this.focus();
   3915       }
   3916     };
   3917 
   3918     this.saveTarget = function (node) {
   3919       $editable.data('target', node);
   3920     };
   3921 
   3922     this.clearTarget = function () {
   3923       $editable.removeData('target');
   3924     };
   3925 
   3926     this.restoreTarget = function () {
   3927       return $editable.data('target');
   3928     };
   3929 
   3930     /**
   3931      * currentStyle
   3932      *
   3933      * current style
   3934      * @return {Object|Boolean} unfocus
   3935      */
   3936     this.currentStyle = function () {
   3937       var rng = range.create();
   3938       if (rng) {
   3939         rng = rng.normalize();
   3940       }
   3941       return rng ? style.current(rng) : style.fromNode($editable);
   3942     };
   3943 
   3944     /**
   3945      * style from node
   3946      *
   3947      * @param {jQuery} $node
   3948      * @return {Object}
   3949      */
   3950     this.styleFromNode = function ($node) {
   3951       return style.fromNode($node);
   3952     };
   3953 
   3954     /**
   3955      * undo
   3956      */
   3957     this.undo = function () {
   3958       context.triggerEvent('before.command', $editable.html());
   3959       history.undo();
   3960       context.triggerEvent('change', $editable.html());
   3961     };
   3962     context.memo('help.undo', lang.help.undo);
   3963 
   3964     /**
   3965      * redo
   3966      */
   3967     this.redo = function () {
   3968       context.triggerEvent('before.command', $editable.html());
   3969       history.redo();
   3970       context.triggerEvent('change', $editable.html());
   3971     };
   3972     context.memo('help.redo', lang.help.redo);
   3973 
   3974     /**
   3975      * before command
   3976      */
   3977     var beforeCommand = this.beforeCommand = function () {
   3978       context.triggerEvent('before.command', $editable.html());
   3979       // keep focus on editable before command execution
   3980       self.focus();
   3981     };
   3982 
   3983     /**
   3984      * after command
   3985      * @param {Boolean} isPreventTrigger
   3986      */
   3987     var afterCommand = this.afterCommand = function (isPreventTrigger) {
   3988       history.recordUndo();
   3989       if (!isPreventTrigger) {
   3990         context.triggerEvent('change', $editable.html());
   3991       }
   3992     };
   3993 
   3994     /* jshint ignore:start */
   3995     // native commands(with execCommand), generate function for execCommand
   3996     var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript',
   3997                     'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull',
   3998                     'formatBlock', 'removeFormat',
   3999                     'backColor', 'foreColor', 'fontName'];
   4000 
   4001     for (var idx = 0, len = commands.length; idx < len; idx ++) {
   4002       this[commands[idx]] = (function (sCmd) {
   4003         return function (value) {
   4004           beforeCommand();
   4005           document.execCommand(sCmd, false, value);
   4006           afterCommand(true);
   4007         };
   4008       })(commands[idx]);
   4009       context.memo('help.' + commands[idx], lang.help[commands[idx]]);
   4010     }
   4011     /* jshint ignore:end */
   4012 
   4013     /**
   4014      * handle tab key
   4015      */
   4016     this.tab = function () {
   4017       var rng = this.createRange();
   4018       if (rng.isCollapsed() && rng.isOnCell()) {
   4019         table.tab(rng);
   4020       } else {
   4021         beforeCommand();
   4022         typing.insertTab(rng, options.tabSize);
   4023         afterCommand();
   4024       }
   4025     };
   4026     context.memo('help.tab', lang.help.tab);
   4027 
   4028     /**
   4029      * handle shift+tab key
   4030      */
   4031     this.untab = function () {
   4032       var rng = this.createRange();
   4033       if (rng.isCollapsed() && rng.isOnCell()) {
   4034         table.tab(rng, true);
   4035       }
   4036     };
   4037     context.memo('help.untab', lang.help.untab);
   4038 
   4039     /**
   4040      * run given function between beforeCommand and afterCommand
   4041      */
   4042     this.wrapCommand = function (fn) {
   4043       return function () {
   4044         beforeCommand();
   4045         fn.apply(self, arguments);
   4046         afterCommand();
   4047       };
   4048     };
   4049 
   4050     /**
   4051      * insert paragraph
   4052      */
   4053     this.insertParagraph = this.wrapCommand(function () {
   4054       typing.insertParagraph(editable);
   4055     });
   4056     context.memo('help.insertParagraph', lang.help.insertParagraph);
   4057 
   4058     this.insertOrderedList = this.wrapCommand(function () {
   4059       bullet.insertOrderedList(editable);
   4060     });
   4061     context.memo('help.insertOrderedList', lang.help.insertOrderedList);
   4062 
   4063     this.insertUnorderedList = this.wrapCommand(function () {
   4064       bullet.insertUnorderedList(editable);
   4065     });
   4066     context.memo('help.insertUnorderedList', lang.help.insertUnorderedList);
   4067 
   4068     this.indent = this.wrapCommand(function () {
   4069       bullet.indent(editable);
   4070     });
   4071     context.memo('help.indent', lang.help.indent);
   4072 
   4073     this.outdent = this.wrapCommand(function () {
   4074       bullet.outdent(editable);
   4075     });
   4076     context.memo('help.outdent', lang.help.outdent);
   4077 
   4078     /**
   4079      * insert image
   4080      *
   4081      * @param {String} src
   4082      * @param {String|Function} param
   4083      * @return {Promise}
   4084      */
   4085     this.insertImage = function (src, param) {
   4086       return async.createImage(src, param).then(function ($image) {
   4087         beforeCommand();
   4088 
   4089         if (typeof param === 'function') {
   4090           param($image);
   4091         } else {
   4092           if (typeof param === 'string') {
   4093             $image.attr('data-filename', param);
   4094           }
   4095           $image.css('width', Math.min($editable.width(), $image.width()));
   4096         }
   4097 
   4098         $image.show();
   4099         range.create(editable).insertNode($image[0]);
   4100         range.createFromNodeAfter($image[0]).select();
   4101         afterCommand();
   4102       }).fail(function (e) {
   4103         context.triggerEvent('image.upload.error', e);
   4104       });
   4105     };
   4106 
   4107     /**
   4108      * insertImages
   4109      * @param {File[]} files
   4110      */
   4111     this.insertImages = function (files) {
   4112       $.each(files, function (idx, file) {
   4113         var filename = file.name;
   4114         if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) {
   4115           context.triggerEvent('image.upload.error', lang.image.maximumFileSizeError);
   4116         } else {
   4117           async.readFileAsDataURL(file).then(function (dataURL) {
   4118             return self.insertImage(dataURL, filename);
   4119           }).fail(function () {
   4120             context.triggerEvent('image.upload.error');
   4121           });
   4122         }
   4123       });
   4124     };
   4125 
   4126     /**
   4127      * insertImagesOrCallback
   4128      * @param {File[]} files
   4129      */
   4130     this.insertImagesOrCallback = function (files) {
   4131       var callbacks = options.callbacks;
   4132 
   4133       // If onImageUpload options setted
   4134       if (callbacks.onImageUpload) {
   4135         context.triggerEvent('image.upload', files);
   4136       // else insert Image as dataURL
   4137       } else {
   4138         this.insertImages(files);
   4139       }
   4140     };
   4141 
   4142     /**
   4143      * insertNode
   4144      * insert node
   4145      * @param {Node} node
   4146      */
   4147     this.insertNode = this.wrapCommand(function (node) {
   4148       var rng = this.createRange();
   4149       rng.insertNode(node);
   4150       range.createFromNodeAfter(node).select();
   4151     });
   4152 
   4153     /**
   4154      * insert text
   4155      * @param {String} text
   4156      */
   4157     this.insertText = this.wrapCommand(function (text) {
   4158       var rng = this.createRange();
   4159       var textNode = rng.insertNode(dom.createText(text));
   4160       range.create(textNode, dom.nodeLength(textNode)).select();
   4161     });
   4162 
   4163     /**
   4164      * return selected plain text
   4165      * @return {String} text
   4166      */
   4167     this.getSelectedText = function () {
   4168       var rng = this.createRange();
   4169 
   4170       // if range on anchor, expand range with anchor
   4171       if (rng.isOnAnchor()) {
   4172         rng = range.createFromNode(dom.ancestor(rng.sc, dom.isAnchor));
   4173       }
   4174 
   4175       return rng.toString();
   4176     };
   4177 
   4178     /**
   4179      * paste HTML
   4180      * @param {String} markup
   4181      */
   4182     this.pasteHTML = this.wrapCommand(function (markup) {
   4183       var contents = this.createRange().pasteHTML(markup);
   4184       range.createFromNodeAfter(list.last(contents)).select();
   4185     });
   4186 
   4187     /**
   4188      * formatBlock
   4189      *
   4190      * @param {String} tagName
   4191      */
   4192     this.formatBlock = this.wrapCommand(function (tagName) {
   4193       // [workaround] for MSIE, IE need `<`
   4194       tagName = agent.isMSIE ? '<' + tagName + '>' : tagName;
   4195       document.execCommand('FormatBlock', false, tagName);
   4196     });
   4197 
   4198     this.formatPara = function () {
   4199       this.formatBlock('P');
   4200     };
   4201     context.memo('help.formatPara', lang.help.formatPara);
   4202 
   4203     /* jshint ignore:start */
   4204     for (var idx = 1; idx <= 6; idx ++) {
   4205       this['formatH' + idx] = function (idx) {
   4206         return function () {
   4207           this.formatBlock('H' + idx);
   4208         };
   4209       }(idx);
   4210       context.memo('help.formatH'+idx, lang.help['formatH' + idx]);
   4211     };
   4212     /* jshint ignore:end */
   4213 
   4214     /**
   4215      * fontSize
   4216      *
   4217      * @param {String} value - px
   4218      */
   4219     this.fontSize = function (value) {
   4220       var rng = this.createRange();
   4221 
   4222       if (rng && rng.isCollapsed()) {
   4223         var spans = style.styleNodes(rng);
   4224         var firstSpan = list.head(spans);
   4225 
   4226         $(spans).css({
   4227           'font-size': value + 'px'
   4228         });
   4229 
   4230         // [workaround] added styled bogus span for style
   4231         //  - also bogus character needed for cursor position
   4232         if (firstSpan && !dom.nodeLength(firstSpan)) {
   4233           firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR;
   4234           range.createFromNodeAfter(firstSpan.firstChild).select();
   4235           $editable.data(KEY_BOGUS, firstSpan);
   4236         }
   4237       } else {
   4238         beforeCommand();
   4239         $(style.styleNodes(rng)).css({
   4240           'font-size': value + 'px'
   4241         });
   4242         afterCommand();
   4243       }
   4244     };
   4245 
   4246     /**
   4247      * insert horizontal rule
   4248      */
   4249     this.insertHorizontalRule = this.wrapCommand(function () {
   4250       var hrNode = this.createRange().insertNode(dom.create('HR'));
   4251       if (hrNode.nextSibling) {
   4252         range.create(hrNode.nextSibling, 0).normalize().select();
   4253       }
   4254     });
   4255     context.memo('help.insertHorizontalRule', lang.help.insertHorizontalRule);
   4256 
   4257     /**
   4258      * remove bogus node and character
   4259      */
   4260     this.removeBogus = function () {
   4261       var bogusNode = $editable.data(KEY_BOGUS);
   4262       if (!bogusNode) {
   4263         return;
   4264       }
   4265 
   4266       var textNode = list.find(list.from(bogusNode.childNodes), dom.isText);
   4267 
   4268       var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR);
   4269       if (bogusCharIdx !== -1) {
   4270         textNode.deleteData(bogusCharIdx, 1);
   4271       }
   4272 
   4273       if (dom.isEmpty(bogusNode)) {
   4274         dom.remove(bogusNode);
   4275       }
   4276 
   4277       $editable.removeData(KEY_BOGUS);
   4278     };
   4279 
   4280     /**
   4281      * lineHeight
   4282      * @param {String} value
   4283      */
   4284     this.lineHeight = this.wrapCommand(function (value) {
   4285       style.stylePara(this.createRange(), {
   4286         lineHeight: value
   4287       });
   4288     });
   4289 
   4290     /**
   4291      * unlink
   4292      *
   4293      * @type command
   4294      */
   4295     this.unlink = function () {
   4296       var rng = this.createRange();
   4297       if (rng.isOnAnchor()) {
   4298         var anchor = dom.ancestor(rng.sc, dom.isAnchor);
   4299         rng = range.createFromNode(anchor);
   4300         rng.select();
   4301 
   4302         beforeCommand();
   4303         document.execCommand('unlink');
   4304         afterCommand();
   4305       }
   4306     };
   4307 
   4308     /**
   4309      * create link (command)
   4310      *
   4311      * @param {Object} linkInfo
   4312      */
   4313     this.createLink = this.wrapCommand(function (linkInfo) {
   4314       var linkUrl = linkInfo.url;
   4315       var linkText = linkInfo.text;
   4316       var isNewWindow = linkInfo.isNewWindow;
   4317       var rng = linkInfo.range || this.createRange();
   4318       var isTextChanged = rng.toString() !== linkText;
   4319 
   4320       // handle spaced urls from input
   4321       if (typeof linkUrl === 'string') {
   4322         linkUrl = linkUrl.trim();
   4323       }
   4324 
   4325       if (options.onCreateLink) {
   4326         linkUrl = options.onCreateLink(linkUrl);
   4327       }
   4328 
   4329       var anchors = [];
   4330       if (isTextChanged) {
   4331         rng = rng.deleteContents();
   4332         var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0]);
   4333         anchors.push(anchor);
   4334       } else {
   4335         anchors = style.styleNodes(rng, {
   4336           nodeName: 'A',
   4337           expandClosestSibling: true,
   4338           onlyPartialContains: true
   4339         });
   4340       }
   4341 
   4342       $.each(anchors, function (idx, anchor) {
   4343         // if url doesn't match an URL schema, set http:// as default
   4344         linkUrl = /^[A-Za-z][A-Za-z0-9+-.]*\:[\/\/]?/.test(linkUrl) ?
   4345           linkUrl : 'http://' + linkUrl;
   4346 
   4347         $(anchor).attr('href', linkUrl);
   4348         if (isNewWindow) {
   4349           $(anchor).attr('target', '_blank');
   4350         } else {
   4351           $(anchor).removeAttr('target');
   4352         }
   4353       });
   4354 
   4355       var startRange = range.createFromNodeBefore(list.head(anchors));
   4356       var startPoint = startRange.getStartPoint();
   4357       var endRange = range.createFromNodeAfter(list.last(anchors));
   4358       var endPoint = endRange.getEndPoint();
   4359 
   4360       range.create(
   4361         startPoint.node,
   4362         startPoint.offset,
   4363         endPoint.node,
   4364         endPoint.offset
   4365       ).select();
   4366     });
   4367 
   4368     /**
   4369      * returns link info
   4370      *
   4371      * @return {Object}
   4372      * @return {WrappedRange} return.range
   4373      * @return {String} return.text
   4374      * @return {Boolean} [return.isNewWindow=true]
   4375      * @return {String} [return.url=""]
   4376      */
   4377     this.getLinkInfo = function () {
   4378       var rng = this.createRange().expand(dom.isAnchor);
   4379 
   4380       // Get the first anchor on range(for edit).
   4381       var $anchor = $(list.head(rng.nodes(dom.isAnchor)));
   4382 
   4383       return {
   4384         range: rng,
   4385         text: rng.toString(),
   4386         isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : false,
   4387         url: $anchor.length ? $anchor.attr('href') : ''
   4388       };
   4389     };
   4390 
   4391     /**
   4392      * setting color
   4393      *
   4394      * @param {Object} sObjColor  color code
   4395      * @param {String} sObjColor.foreColor foreground color
   4396      * @param {String} sObjColor.backColor background color
   4397      */
   4398     this.color = this.wrapCommand(function (colorInfo) {
   4399       var foreColor = colorInfo.foreColor;
   4400       var backColor = colorInfo.backColor;
   4401 
   4402       if (foreColor) { document.execCommand('foreColor', false, foreColor); }
   4403       if (backColor) { document.execCommand('backColor', false, backColor); }
   4404     });
   4405 
   4406     /**
   4407      * insert Table
   4408      *
   4409      * @param {String} dimension of table (ex : "5x5")
   4410      */
   4411     this.insertTable = this.wrapCommand(function (dim) {
   4412       var dimension = dim.split('x');
   4413 
   4414       var rng = this.createRange().deleteContents();
   4415       rng.insertNode(table.createTable(dimension[0], dimension[1], options));
   4416     });
   4417 
   4418     /**
   4419      * float me
   4420      *
   4421      * @param {String} value
   4422      */
   4423     this.floatMe = this.wrapCommand(function (value) {
   4424       var $target = $(this.restoreTarget());
   4425       $target.css('float', value);
   4426     });
   4427 
   4428     /**
   4429      * resize overlay element
   4430      * @param {String} value
   4431      */
   4432     this.resize = this.wrapCommand(function (value) {
   4433       var $target = $(this.restoreTarget());
   4434       $target.css({
   4435         width: value * 100 + '%',
   4436         height: ''
   4437       });
   4438     });
   4439 
   4440     /**
   4441      * @param {Position} pos
   4442      * @param {jQuery} $target - target element
   4443      * @param {Boolean} [bKeepRatio] - keep ratio
   4444      */
   4445     this.resizeTo = function (pos, $target, bKeepRatio) {
   4446       var imageSize;
   4447       if (bKeepRatio) {
   4448         var newRatio = pos.y / pos.x;
   4449         var ratio = $target.data('ratio');
   4450         imageSize = {
   4451           width: ratio > newRatio ? pos.x : pos.y / ratio,
   4452           height: ratio > newRatio ? pos.x * ratio : pos.y
   4453         };
   4454       } else {
   4455         imageSize = {
   4456           width: pos.x,
   4457           height: pos.y
   4458         };
   4459       }
   4460 
   4461       $target.css(imageSize);
   4462     };
   4463 
   4464     /**
   4465      * remove media object
   4466      */
   4467     this.removeMedia = this.wrapCommand(function () {
   4468       var $target = $(this.restoreTarget()).detach();
   4469       context.triggerEvent('media.delete', $target, $editable);
   4470     });
   4471 
   4472     /**
   4473      * returns whether editable area has focus or not.
   4474      */
   4475     this.hasFocus = function () {
   4476       return $editable.is(':focus');
   4477     };
   4478 
   4479     /**
   4480      * set focus
   4481      */
   4482     this.focus = function () {
   4483       // [workaround] Screen will move when page is scolled in IE.
   4484       //  - do focus when not focused
   4485       if (!this.hasFocus()) {
   4486         $editable.focus();
   4487       }
   4488     };
   4489 
   4490     /**
   4491      * returns whether contents is empty or not.
   4492      * @return {Boolean}
   4493      */
   4494     this.isEmpty = function () {
   4495       return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html();
   4496     };
   4497 
   4498     /**
   4499      * Removes all contents and restores the editable instance to an _emptyPara_.
   4500      */
   4501     this.empty = function () {
   4502       context.invoke('code', dom.emptyPara);
   4503     };
   4504   };
   4505 
   4506   var Clipboard = function (context) {
   4507     var self = this;
   4508 
   4509     var $editable = context.layoutInfo.editable;
   4510 
   4511     this.events = {
   4512       'summernote.keydown': function (we, e) {
   4513         if (self.needKeydownHook()) {
   4514           if ((e.ctrlKey || e.metaKey) && e.keyCode === key.code.V) {
   4515             context.invoke('editor.saveRange');
   4516             self.$paste.focus();
   4517 
   4518             setTimeout(function () {
   4519               self.pasteByHook();
   4520             }, 0);
   4521           }
   4522         }
   4523       }
   4524     };
   4525 
   4526     this.needKeydownHook = function () {
   4527       return (agent.isMSIE && agent.browserVersion > 10) || agent.isFF;
   4528     };
   4529 
   4530     this.initialize = function () {
   4531       // [workaround] getting image from clipboard
   4532       //  - IE11 and Firefox: CTRL+v hook
   4533       //  - Webkit: event.clipboardData
   4534       if (this.needKeydownHook()) {
   4535         this.$paste = $('<div tabindex="-1" />').attr('contenteditable', true).css({
   4536           position: 'absolute',
   4537           left: -100000,
   4538           opacity: 0
   4539         });
   4540         $editable.before(this.$paste);
   4541 
   4542         this.$paste.on('paste', function (event) {
   4543           context.triggerEvent('paste', event);
   4544         });
   4545       } else {
   4546         $editable.on('paste', this.pasteByEvent);
   4547       }
   4548     };
   4549 
   4550     this.destroy = function () {
   4551       if (this.needKeydownHook()) {
   4552         this.$paste.remove();
   4553         this.$paste = null;
   4554       }
   4555     };
   4556 
   4557     this.pasteByHook = function () {
   4558       var node = this.$paste[0].firstChild;
   4559 
   4560       if (dom.isImg(node)) {
   4561         var dataURI = node.src;
   4562         var decodedData = atob(dataURI.split(',')[1]);
   4563         var array = new Uint8Array(decodedData.length);
   4564         for (var i = 0; i < decodedData.length; i++) {
   4565           array[i] = decodedData.charCodeAt(i);
   4566         }
   4567 
   4568         var blob = new Blob([array], { type: 'image/png' });
   4569         blob.name = 'clipboard.png';
   4570 
   4571         context.invoke('editor.restoreRange');
   4572         context.invoke('editor.focus');
   4573         context.invoke('editor.insertImagesOrCallback', [blob]);
   4574       } else {
   4575         var pasteContent = $('<div />').html(this.$paste.html()).html();
   4576         context.invoke('editor.restoreRange');
   4577         context.invoke('editor.focus');
   4578 
   4579         if (pasteContent) {
   4580           context.invoke('editor.pasteHTML', pasteContent);
   4581         }
   4582       }
   4583 
   4584       this.$paste.empty();
   4585     };
   4586 
   4587     /**
   4588      * paste by clipboard event
   4589      *
   4590      * @param {Event} event
   4591      */
   4592     this.pasteByEvent = function (event) {
   4593       var clipboardData = event.originalEvent.clipboardData;
   4594       if (clipboardData && clipboardData.items && clipboardData.items.length) {
   4595         var item = list.head(clipboardData.items);
   4596         if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
   4597           context.invoke('editor.insertImagesOrCallback', [item.getAsFile()]);
   4598         }
   4599         context.invoke('editor.afterCommand');
   4600       }
   4601     };
   4602   };
   4603 
   4604   var Dropzone = function (context) {
   4605     var $document = $(document);
   4606     var $editor = context.layoutInfo.editor;
   4607     var $editable = context.layoutInfo.editable;
   4608     var options = context.options;
   4609     var lang = options.langInfo;
   4610     var documentEventHandlers = {};
   4611 
   4612     var $dropzone = $([
   4613       '<div class="note-dropzone">',
   4614       '  <div class="note-dropzone-message"/>',
   4615       '</div>'
   4616     ].join('')).prependTo($editor);
   4617 
   4618     var detachDocumentEvent = function () {
   4619       Object.keys(documentEventHandlers).forEach(function (key) {
   4620         $document.off(key.substr(2).toLowerCase(), documentEventHandlers[key]);
   4621       });
   4622       documentEventHandlers = {};
   4623     };
   4624 
   4625     /**
   4626      * attach Drag and Drop Events
   4627      */
   4628     this.initialize = function () {
   4629       if (options.disableDragAndDrop) {
   4630         // prevent default drop event
   4631         documentEventHandlers.onDrop = function (e) {
   4632           e.preventDefault();
   4633         };
   4634         $document.on('drop', documentEventHandlers.onDrop);
   4635       } else {
   4636         this.attachDragAndDropEvent();
   4637       }
   4638     };
   4639 
   4640     /**
   4641      * attach Drag and Drop Events
   4642      */
   4643     this.attachDragAndDropEvent = function () {
   4644       var collection = $(),
   4645           $dropzoneMessage = $dropzone.find('.note-dropzone-message');
   4646 
   4647       documentEventHandlers.onDragenter = function (e) {
   4648         var isCodeview = context.invoke('codeview.isActivated');
   4649         var hasEditorSize = $editor.width() > 0 && $editor.height() > 0;
   4650         if (!isCodeview && !collection.length && hasEditorSize) {
   4651           $editor.addClass('dragover');
   4652           $dropzone.width($editor.width());
   4653           $dropzone.height($editor.height());
   4654           $dropzoneMessage.text(lang.image.dragImageHere);
   4655         }
   4656         collection = collection.add(e.target);
   4657       };
   4658 
   4659       documentEventHandlers.onDragleave = function (e) {
   4660         collection = collection.not(e.target);
   4661         if (!collection.length) {
   4662           $editor.removeClass('dragover');
   4663         }
   4664       };
   4665 
   4666       documentEventHandlers.onDrop = function () {
   4667         collection = $();
   4668         $editor.removeClass('dragover');
   4669       };
   4670 
   4671       // show dropzone on dragenter when dragging a object to document
   4672       // -but only if the editor is visible, i.e. has a positive width and height
   4673       $document.on('dragenter', documentEventHandlers.onDragenter)
   4674         .on('dragleave', documentEventHandlers.onDragleave)
   4675         .on('drop', documentEventHandlers.onDrop);
   4676 
   4677       // change dropzone's message on hover.
   4678       $dropzone.on('dragenter', function () {
   4679         $dropzone.addClass('hover');
   4680         $dropzoneMessage.text(lang.image.dropImage);
   4681       }).on('dragleave', function () {
   4682         $dropzone.removeClass('hover');
   4683         $dropzoneMessage.text(lang.image.dragImageHere);
   4684       });
   4685 
   4686       // attach dropImage
   4687       $dropzone.on('drop', function (event) {
   4688         var dataTransfer = event.originalEvent.dataTransfer;
   4689 
   4690         if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
   4691           event.preventDefault();
   4692           $editable.focus();
   4693           context.invoke('editor.insertImagesOrCallback', dataTransfer.files);
   4694         } else {
   4695           $.each(dataTransfer.types, function (idx, type) {
   4696             var content = dataTransfer.getData(type);
   4697 
   4698             if (type.toLowerCase().indexOf('text') > -1) {
   4699               context.invoke('editor.pasteHTML', content);
   4700             } else {
   4701               $(content).each(function () {
   4702                 context.invoke('editor.insertNode', this);
   4703               });
   4704             }
   4705           });
   4706         }
   4707       }).on('dragover', false); // prevent default dragover event
   4708     };
   4709 
   4710     this.destroy = function () {
   4711       detachDocumentEvent();
   4712     };
   4713   };
   4714 
   4715 
   4716   var CodeMirror;
   4717   if (agent.hasCodeMirror) {
   4718     if (agent.isSupportAmd) {
   4719       require(['codemirror'], function (cm) {
   4720         CodeMirror = cm;
   4721       });
   4722     } else {
   4723       CodeMirror = window.CodeMirror;
   4724     }
   4725   }
   4726 
   4727   /**
   4728    * @class Codeview
   4729    */
   4730   var Codeview = function (context) {
   4731     var $editor = context.layoutInfo.editor;
   4732     var $editable = context.layoutInfo.editable;
   4733     var $codable = context.layoutInfo.codable;
   4734     var options = context.options;
   4735 
   4736     this.sync = function () {
   4737       var isCodeview = this.isActivated();
   4738       if (isCodeview && agent.hasCodeMirror) {
   4739         $codable.data('cmEditor').save();
   4740       }
   4741     };
   4742 
   4743     /**
   4744      * @return {Boolean}
   4745      */
   4746     this.isActivated = function () {
   4747       return $editor.hasClass('codeview');
   4748     };
   4749 
   4750     /**
   4751      * toggle codeview
   4752      */
   4753     this.toggle = function () {
   4754       if (this.isActivated()) {
   4755         this.deactivate();
   4756       } else {
   4757         this.activate();
   4758       }
   4759       context.triggerEvent('codeview.toggled');
   4760     };
   4761 
   4762     /**
   4763      * activate code view
   4764      */
   4765     this.activate = function () {
   4766       $codable.val(dom.html($editable, options.prettifyHtml));
   4767       $codable.height($editable.height());
   4768 
   4769       context.invoke('toolbar.updateCodeview', true);
   4770       $editor.addClass('codeview');
   4771       $codable.focus();
   4772 
   4773       // activate CodeMirror as codable
   4774       if (agent.hasCodeMirror) {
   4775         var cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror);
   4776 
   4777         // CodeMirror TernServer
   4778         if (options.codemirror.tern) {
   4779           var server = new CodeMirror.TernServer(options.codemirror.tern);
   4780           cmEditor.ternServer = server;
   4781           cmEditor.on('cursorActivity', function (cm) {
   4782             server.updateArgHints(cm);
   4783           });
   4784         }
   4785 
   4786         // CodeMirror hasn't Padding.
   4787         cmEditor.setSize(null, $editable.outerHeight());
   4788         $codable.data('cmEditor', cmEditor);
   4789       }
   4790     };
   4791 
   4792     /**
   4793      * deactivate code view
   4794      */
   4795     this.deactivate = function () {
   4796       // deactivate CodeMirror as codable
   4797       if (agent.hasCodeMirror) {
   4798         var cmEditor = $codable.data('cmEditor');
   4799         $codable.val(cmEditor.getValue());
   4800         cmEditor.toTextArea();
   4801       }
   4802 
   4803       var value = dom.value($codable, options.prettifyHtml) || dom.emptyPara;
   4804       var isChange = $editable.html() !== value;
   4805 
   4806       $editable.html(value);
   4807       $editable.height(options.height ? $codable.height() : 'auto');
   4808       $editor.removeClass('codeview');
   4809 
   4810       if (isChange) {
   4811         context.triggerEvent('change', $editable.html(), $editable);
   4812       }
   4813 
   4814       $editable.focus();
   4815 
   4816       context.invoke('toolbar.updateCodeview', false);
   4817     };
   4818 
   4819     this.destroy = function () {
   4820       if (this.isActivated()) {
   4821         this.deactivate();
   4822       }
   4823     };
   4824   };
   4825 
   4826   var EDITABLE_PADDING = 24;
   4827 
   4828   var Statusbar = function (context) {
   4829     var $document = $(document);
   4830     var $statusbar = context.layoutInfo.statusbar;
   4831     var $editable = context.layoutInfo.editable;
   4832     var options = context.options;
   4833 
   4834     this.initialize = function () {
   4835       if (options.airMode || options.disableResizeEditor) {
   4836         return;
   4837       }
   4838 
   4839       $statusbar.on('mousedown', function (event) {
   4840         event.preventDefault();
   4841         event.stopPropagation();
   4842 
   4843         var editableTop = $editable.offset().top - $document.scrollTop();
   4844 
   4845         $document.on('mousemove', function (event) {
   4846           var height = event.clientY - (editableTop + EDITABLE_PADDING);
   4847 
   4848           height = (options.minheight > 0) ? Math.max(height, options.minheight) : height;
   4849           height = (options.maxHeight > 0) ? Math.min(height, options.maxHeight) : height;
   4850 
   4851           $editable.height(height);
   4852         }).one('mouseup', function () {
   4853           $document.off('mousemove');
   4854         });
   4855       });
   4856     };
   4857 
   4858     this.destroy = function () {
   4859       $statusbar.off();
   4860       $statusbar.remove();
   4861     };
   4862   };
   4863 
   4864   var Fullscreen = function (context) {
   4865     var $editor = context.layoutInfo.editor;
   4866     var $toolbar = context.layoutInfo.toolbar;
   4867     var $editable = context.layoutInfo.editable;
   4868     var $codable = context.layoutInfo.codable;
   4869 
   4870     var $window = $(window);
   4871     var $scrollbar = $('html, body');
   4872 
   4873     /**
   4874      * toggle fullscreen
   4875      */
   4876     this.toggle = function () {
   4877       var resize = function (size) {
   4878         $editable.css('height', size.h);
   4879         $codable.css('height', size.h);
   4880         if ($codable.data('cmeditor')) {
   4881           $codable.data('cmeditor').setsize(null, size.h);
   4882         }
   4883       };
   4884 
   4885       $editor.toggleClass('fullscreen');
   4886       if (this.isFullscreen()) {
   4887         $editable.data('orgHeight', $editable.css('height'));
   4888 
   4889         $window.on('resize', function () {
   4890           resize({
   4891             h: $window.height() - $toolbar.outerHeight()
   4892           });
   4893         }).trigger('resize');
   4894 
   4895         $scrollbar.css('overflow', 'hidden');
   4896       } else {
   4897         $window.off('resize');
   4898         resize({
   4899           h: $editable.data('orgHeight')
   4900         });
   4901         $scrollbar.css('overflow', 'visible');
   4902       }
   4903 
   4904       context.invoke('toolbar.updateFullscreen', this.isFullscreen());
   4905     };
   4906 
   4907     this.isFullscreen = function () {
   4908       return $editor.hasClass('fullscreen');
   4909     };
   4910   };
   4911 
   4912   var Handle = function (context) {
   4913     var self = this;
   4914 
   4915     var $document = $(document);
   4916     var $editingArea = context.layoutInfo.editingArea;
   4917     var options = context.options;
   4918 
   4919     this.events = {
   4920       'summernote.mousedown': function (we, e) {
   4921         if (self.update(e.target)) {
   4922           e.preventDefault();
   4923         }
   4924       },
   4925       'summernote.keyup summernote.scroll summernote.change summernote.dialog.shown': function () {
   4926         self.update();
   4927       }
   4928     };
   4929 
   4930     this.initialize = function () {
   4931       this.$handle = $([
   4932         '<div class="note-handle">',
   4933         '<div class="note-control-selection">',
   4934         '<div class="note-control-selection-bg"></div>',
   4935         '<div class="note-control-holder note-control-nw"></div>',
   4936         '<div class="note-control-holder note-control-ne"></div>',
   4937         '<div class="note-control-holder note-control-sw"></div>',
   4938         '<div class="',
   4939         (options.disableResizeImage ? 'note-control-holder' : 'note-control-sizing'),
   4940         ' note-control-se"></div>',
   4941         (options.disableResizeImage ? '' : '<div class="note-control-selection-info"></div>'),
   4942         '</div>',
   4943         '</div>'
   4944       ].join('')).prependTo($editingArea);
   4945 
   4946       this.$handle.on('mousedown', function (event) {
   4947         if (dom.isControlSizing(event.target)) {
   4948           event.preventDefault();
   4949           event.stopPropagation();
   4950 
   4951           var $target = self.$handle.find('.note-control-selection').data('target'),
   4952               posStart = $target.offset(),
   4953               scrollTop = $document.scrollTop();
   4954 
   4955           $document.on('mousemove', function (event) {
   4956             context.invoke('editor.resizeTo', {
   4957               x: event.clientX - posStart.left,
   4958               y: event.clientY - (posStart.top - scrollTop)
   4959             }, $target, !event.shiftKey);
   4960 
   4961             self.update($target[0]);
   4962           }).one('mouseup', function (e) {
   4963             e.preventDefault();
   4964             $document.off('mousemove');
   4965             context.invoke('editor.afterCommand');
   4966           });
   4967 
   4968           if (!$target.data('ratio')) { // original ratio.
   4969             $target.data('ratio', $target.height() / $target.width());
   4970           }
   4971         }
   4972       });
   4973     };
   4974 
   4975     this.destroy = function () {
   4976       this.$handle.remove();
   4977     };
   4978 
   4979     this.update = function (target) {
   4980       var isImage = dom.isImg(target);
   4981       var $selection = this.$handle.find('.note-control-selection');
   4982 
   4983       context.invoke('imagePopover.update', target);
   4984 
   4985       if (isImage) {
   4986         var $image = $(target);
   4987         var pos = $image.position();
   4988 
   4989         // include margin
   4990         var imageSize = {
   4991           w: $image.outerWidth(true),
   4992           h: $image.outerHeight(true)
   4993         };
   4994 
   4995         $selection.css({
   4996           display: 'block',
   4997           left: pos.left,
   4998           top: pos.top,
   4999           width: imageSize.w,
   5000           height: imageSize.h
   5001         }).data('target', $image); // save current image element.
   5002 
   5003         var sizingText = imageSize.w + 'x' + imageSize.h;
   5004         $selection.find('.note-control-selection-info').text(sizingText);
   5005         context.invoke('editor.saveTarget', target);
   5006       } else {
   5007         this.hide();
   5008       }
   5009 
   5010       return isImage;
   5011     };
   5012 
   5013     /**
   5014      * hide
   5015      *
   5016      * @param {jQuery} $handle
   5017      */
   5018     this.hide = function () {
   5019       context.invoke('editor.clearTarget');
   5020       this.$handle.children().hide();
   5021     };
   5022   };
   5023 
   5024   var AutoLink = function (context) {
   5025     var self = this;
   5026     var defaultScheme = 'http://';
   5027     var linkPattern = /^([A-Za-z][A-Za-z0-9+-.]*\:[\/\/]?|mailto:[A-Z0-9._%+-]+@)?(www\.)?(.+)$/i;
   5028 
   5029     this.events = {
   5030       'summernote.keyup': function (we, e) {
   5031         if (!e.isDefaultPrevented()) {
   5032           self.handleKeyup(e);
   5033         }
   5034       },
   5035       'summernote.keydown': function (we, e) {
   5036         self.handleKeydown(e);
   5037       }
   5038     };
   5039 
   5040     this.initialize = function () {
   5041       this.lastWordRange = null;
   5042     };
   5043 
   5044     this.destroy = function () {
   5045       this.lastWordRange = null;
   5046     };
   5047 
   5048     this.replace = function () {
   5049       if (!this.lastWordRange) {
   5050         return;
   5051       }
   5052 
   5053       var keyword = this.lastWordRange.toString();
   5054       var match = keyword.match(linkPattern);
   5055 
   5056       if (match && (match[1] || match[2])) {
   5057         var link = match[1] ? keyword : defaultScheme + keyword;
   5058         var node = $('<a />').html(keyword).attr('href', link)[0];
   5059 
   5060         this.lastWordRange.insertNode(node);
   5061         this.lastWordRange = null;
   5062         context.invoke('editor.focus');
   5063       }
   5064 
   5065     };
   5066 
   5067     this.handleKeydown = function (e) {
   5068       if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) {
   5069         var wordRange = context.invoke('editor.createRange').getWordRange();
   5070         this.lastWordRange = wordRange;
   5071       }
   5072     };
   5073 
   5074     this.handleKeyup = function (e) {
   5075       if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) {
   5076         this.replace();
   5077       }
   5078     };
   5079   };
   5080 
   5081   /**
   5082    * textarea auto sync.
   5083    */
   5084   var AutoSync = function (context) {
   5085     var $note = context.layoutInfo.note;
   5086 
   5087     this.events = {
   5088       'summernote.change': function () {
   5089         $note.val(context.invoke('code'));
   5090       }
   5091     };
   5092 
   5093     this.shouldInitialize = function () {
   5094       return dom.isTextarea($note[0]);
   5095     };
   5096   };
   5097 
   5098   var Placeholder = function (context) {
   5099     var self = this;
   5100     var $editingArea = context.layoutInfo.editingArea;
   5101     var options = context.options;
   5102 
   5103     this.events = {
   5104       'summernote.init summernote.change': function () {
   5105         self.update();
   5106       },
   5107       'summernote.codeview.toggled': function () {
   5108         self.update();
   5109       }
   5110     };
   5111 
   5112     this.shouldInitialize = function () {
   5113       return !!options.placeholder;
   5114     };
   5115 
   5116     this.initialize = function () {
   5117       this.$placeholder = $('<div class="note-placeholder">');
   5118       this.$placeholder.on('click', function () {
   5119         context.invoke('focus');
   5120       }).text(options.placeholder).prependTo($editingArea);
   5121     };
   5122 
   5123     this.destroy = function () {
   5124       this.$placeholder.remove();
   5125     };
   5126 
   5127     this.update = function () {
   5128       var isShow = !context.invoke('codeview.isActivated') && context.invoke('editor.isEmpty');
   5129       this.$placeholder.toggle(isShow);
   5130     };
   5131   };
   5132 
   5133   var Buttons = function (context) {
   5134     var self = this;
   5135     var ui = $.summernote.ui;
   5136 
   5137     var $toolbar = context.layoutInfo.toolbar;
   5138     var options = context.options;
   5139     var lang = options.langInfo;
   5140 
   5141     var invertedKeyMap = func.invertObject(options.keyMap[agent.isMac ? 'mac' : 'pc']);
   5142 
   5143     var representShortcut = this.representShortcut = function (editorMethod) {
   5144       var shortcut = invertedKeyMap[editorMethod];
   5145       if (!options.shortcuts || !shortcut) {
   5146         return '';
   5147       }
   5148       
   5149       if (agent.isMac) {
   5150         shortcut = shortcut.replace('CMD', '⌘').replace('SHIFT', '⇧');
   5151       }
   5152 
   5153       shortcut = shortcut.replace('BACKSLASH', '\\')
   5154                          .replace('SLASH', '/')
   5155                          .replace('LEFTBRACKET', '[')
   5156                          .replace('RIGHTBRACKET', ']');
   5157 
   5158       return ' (' + shortcut + ')';
   5159     };
   5160 
   5161     this.initialize = function () {
   5162       this.addToolbarButtons();
   5163       this.addImagePopoverButtons();
   5164       this.addLinkPopoverButtons();
   5165       this.fontInstalledMap = {};
   5166     };
   5167 
   5168     this.destroy = function () {
   5169       delete this.fontInstalledMap;
   5170     };
   5171 
   5172     this.isFontInstalled = function (name) {
   5173       if (!self.fontInstalledMap.hasOwnProperty(name)) {
   5174         self.fontInstalledMap[name] = agent.isFontInstalled(name) ||
   5175           list.contains(options.fontNamesIgnoreCheck, name);
   5176       }
   5177 
   5178       return self.fontInstalledMap[name];
   5179     };
   5180 
   5181     this.addToolbarButtons = function () {
   5182       context.memo('button.style', function () {
   5183         return ui.buttonGroup([
   5184           ui.button({
   5185             className: 'dropdown-toggle',
   5186             contents: ui.icon(options.icons.magic) + ' ' + ui.icon(options.icons.caret, 'span'),
   5187             tooltip: lang.style.style,
   5188             data: {
   5189               toggle: 'dropdown'
   5190             }
   5191           }),
   5192           ui.dropdown({
   5193             className: 'dropdown-style',
   5194             items: context.options.styleTags,
   5195             template: function (item) {
   5196 
   5197               if (typeof item === 'string') {
   5198                 item = { tag: item, title: (lang.style.hasOwnProperty(item) ? lang.style[item] : item) };
   5199               }
   5200 
   5201               var tag = item.tag;
   5202               var title = item.title;
   5203               var style = item.style ? ' style="' + item.style + '" ' : '';
   5204               var className = item.className ? ' class="' + item.className + '"' : '';
   5205 
   5206               return '<' + tag + style + className + '>' + title + '</' + tag +  '>';
   5207             },
   5208             click: context.createInvokeHandler('editor.formatBlock')
   5209           })
   5210         ]).render();
   5211       });
   5212 
   5213       context.memo('button.bold', function () {
   5214         return ui.button({
   5215           className: 'note-btn-bold',
   5216           contents: ui.icon(options.icons.bold),
   5217           tooltip: lang.font.bold + representShortcut('bold'),
   5218           click: context.createInvokeHandler('editor.bold')
   5219         }).render();
   5220       });
   5221 
   5222       context.memo('button.italic', function () {
   5223         return ui.button({
   5224           className: 'note-btn-italic',
   5225           contents: ui.icon(options.icons.italic),
   5226           tooltip: lang.font.italic + representShortcut('italic'),
   5227           click: context.createInvokeHandler('editor.italic')
   5228         }).render();
   5229       });
   5230 
   5231       context.memo('button.underline', function () {
   5232         return ui.button({
   5233           className: 'note-btn-underline',
   5234           contents: ui.icon(options.icons.underline),
   5235           tooltip: lang.font.underline + representShortcut('underline'),
   5236           click: context.createInvokeHandler('editor.underline')
   5237         }).render();
   5238       });
   5239 
   5240       context.memo('button.clear', function () {
   5241         return ui.button({
   5242           contents: ui.icon(options.icons.eraser),
   5243           tooltip: lang.font.clear + representShortcut('removeFormat'),
   5244           click: context.createInvokeHandler('editor.removeFormat')
   5245         }).render();
   5246       });
   5247 
   5248       context.memo('button.strikethrough', function () {
   5249         return ui.button({
   5250           className: 'note-btn-strikethrough',
   5251           contents: ui.icon(options.icons.strikethrough),
   5252           tooltip: lang.font.strikethrough + representShortcut('strikethrough'),
   5253           click: context.createInvokeHandler('editor.strikethrough')
   5254         }).render();
   5255       });
   5256 
   5257       context.memo('button.superscript', function () {
   5258         return ui.button({
   5259           className: 'note-btn-superscript',
   5260           contents: ui.icon(options.icons.superscript),
   5261           tooltip: lang.font.superscript,
   5262           click: context.createInvokeHandler('editor.superscript')
   5263         }).render();
   5264       });
   5265 
   5266       context.memo('button.subscript', function () {
   5267         return ui.button({
   5268           className: 'note-btn-subscript',
   5269           contents: ui.icon(options.icons.subscript),
   5270           tooltip: lang.font.subscript,
   5271           click: context.createInvokeHandler('editor.subscript')
   5272         }).render();
   5273       });
   5274 
   5275       context.memo('button.fontname', function () {
   5276         return ui.buttonGroup([
   5277           ui.button({
   5278             className: 'dropdown-toggle',
   5279             contents: '<span class="note-current-fontname"/> ' + ui.icon(options.icons.caret, 'span'),
   5280             tooltip: lang.font.name,
   5281             data: {
   5282               toggle: 'dropdown'
   5283             }
   5284           }),
   5285           ui.dropdownCheck({
   5286             className: 'dropdown-fontname',
   5287             checkClassName: options.icons.menuCheck,
   5288             items: options.fontNames.filter(self.isFontInstalled),
   5289             template: function (item) {
   5290               return '<span style="font-family:' + item + '">' + item + '</span>';
   5291             },
   5292             click: context.createInvokeHandler('editor.fontName')
   5293           })
   5294         ]).render();
   5295       });
   5296 
   5297       context.memo('button.fontsize', function () {
   5298         return ui.buttonGroup([
   5299           ui.button({
   5300             className: 'dropdown-toggle',
   5301             contents: '<span class="note-current-fontsize"/>' + ui.icon(options.icons.caret, 'span'),
   5302             tooltip: lang.font.size,
   5303             data: {
   5304               toggle: 'dropdown'
   5305             }
   5306           }),
   5307           ui.dropdownCheck({
   5308             className: 'dropdown-fontsize',
   5309             checkClassName: options.icons.menuCheck,
   5310             items: options.fontSizes,
   5311             click: context.createInvokeHandler('editor.fontSize')
   5312           })
   5313         ]).render();
   5314       });
   5315 
   5316       context.memo('button.color', function () {
   5317         return ui.buttonGroup({
   5318           className: 'note-color',
   5319           children: [
   5320             ui.button({
   5321               className: 'note-current-color-button',
   5322               contents: ui.icon(options.icons.font + ' note-recent-color'),
   5323               tooltip: lang.color.recent,
   5324               click: function (e) {
   5325                 var $button = $(e.currentTarget);
   5326                 context.invoke('editor.color', {
   5327                   backColor: $button.attr('data-backColor'),
   5328                   foreColor: $button.attr('data-foreColor')
   5329                 });
   5330               },
   5331               callback: function ($button) {
   5332                 var $recentColor = $button.find('.note-recent-color');
   5333                 $recentColor.css('background-color', '#FFFF00');
   5334                 $button.attr('data-backColor', '#FFFF00');
   5335               }
   5336             }),
   5337             ui.button({
   5338               className: 'dropdown-toggle',
   5339               contents: ui.icon(options.icons.caret, 'span'),
   5340               tooltip: lang.color.more,
   5341               data: {
   5342                 toggle: 'dropdown'
   5343               }
   5344             }),
   5345             ui.dropdown({
   5346               items: [
   5347                 '<li>',
   5348                 '<div class="btn-group">',
   5349                 '  <div class="note-palette-title">' + lang.color.background + '</div>',
   5350                 '  <div>',
   5351                 '    <button type="button" class="note-color-reset btn btn-default" data-event="backColor" data-value="inherit">',
   5352                 lang.color.transparent,
   5353                 '    </button>',
   5354                 '  </div>',
   5355                 '  <div class="note-holder" data-event="backColor"/>',
   5356                 '</div>',
   5357                 '<div class="btn-group">',
   5358                 '  <div class="note-palette-title">' + lang.color.foreground + '</div>',
   5359                 '  <div>',
   5360                 '    <button type="button" class="note-color-reset btn btn-default" data-event="removeFormat" data-value="foreColor">',
   5361                 lang.color.resetToDefault,
   5362                 '    </button>',
   5363                 '  </div>',
   5364                 '  <div class="note-holder" data-event="foreColor"/>',
   5365                 '</div>',
   5366                 '</li>'
   5367               ].join(''),
   5368               callback: function ($dropdown) {
   5369                 $dropdown.find('.note-holder').each(function () {
   5370                   var $holder = $(this);
   5371                   $holder.append(ui.palette({
   5372                     colors: options.colors,
   5373                     eventName: $holder.data('event')
   5374                   }).render());
   5375                 });
   5376               },
   5377               click: function (event) {
   5378                 var $button = $(event.target);
   5379                 var eventName = $button.data('event');
   5380                 var value = $button.data('value');
   5381 
   5382                 if (eventName && value) {
   5383                   var key = eventName === 'backColor' ? 'background-color' : 'color';
   5384                   var $color = $button.closest('.note-color').find('.note-recent-color');
   5385                   var $currentButton = $button.closest('.note-color').find('.note-current-color-button');
   5386 
   5387                   $color.css(key, value);
   5388                   $currentButton.attr('data-' + eventName, value);
   5389                   context.invoke('editor.' + eventName, value);
   5390                 }
   5391               }
   5392             })
   5393           ]
   5394         }).render();
   5395       });
   5396 
   5397       context.memo('button.ul',  function () {
   5398         return ui.button({
   5399           contents: ui.icon(options.icons.unorderedlist),
   5400           tooltip: lang.lists.unordered + representShortcut('insertUnorderedList'),
   5401           click: context.createInvokeHandler('editor.insertUnorderedList')
   5402         }).render();
   5403       });
   5404 
   5405       context.memo('button.ol', function () {
   5406         return ui.button({
   5407           contents: ui.icon(options.icons.orderedlist),
   5408           tooltip: lang.lists.ordered + representShortcut('insertOrderedList'),
   5409           click:  context.createInvokeHandler('editor.insertOrderedList')
   5410         }).render();
   5411       });
   5412 
   5413       var justifyLeft = ui.button({
   5414         contents: ui.icon(options.icons.alignLeft),
   5415         tooltip: lang.paragraph.left + representShortcut('justifyLeft'),
   5416         click: context.createInvokeHandler('editor.justifyLeft')
   5417       });
   5418 
   5419       var justifyCenter = ui.button({
   5420         contents: ui.icon(options.icons.alignCenter),
   5421         tooltip: lang.paragraph.center + representShortcut('justifyCenter'),
   5422         click: context.createInvokeHandler('editor.justifyCenter')
   5423       });
   5424 
   5425       var justifyRight = ui.button({
   5426         contents: ui.icon(options.icons.alignRight),
   5427         tooltip: lang.paragraph.right + representShortcut('justifyRight'),
   5428         click: context.createInvokeHandler('editor.justifyRight')
   5429       });
   5430 
   5431       var justifyFull = ui.button({
   5432         contents: ui.icon(options.icons.alignJustify),
   5433         tooltip: lang.paragraph.justify + representShortcut('justifyFull'),
   5434         click: context.createInvokeHandler('editor.justifyFull')
   5435       });
   5436 
   5437       var outdent = ui.button({
   5438         contents: ui.icon(options.icons.outdent),
   5439         tooltip: lang.paragraph.outdent + representShortcut('outdent'),
   5440         click: context.createInvokeHandler('editor.outdent')
   5441       });
   5442 
   5443       var indent = ui.button({
   5444         contents: ui.icon(options.icons.indent),
   5445         tooltip: lang.paragraph.indent + representShortcut('indent'),
   5446         click: context.createInvokeHandler('editor.indent')
   5447       });
   5448 
   5449       context.memo('button.justifyLeft', func.invoke(justifyLeft, 'render'));
   5450       context.memo('button.justifyCenter', func.invoke(justifyCenter, 'render'));
   5451       context.memo('button.justifyRight', func.invoke(justifyRight, 'render'));
   5452       context.memo('button.justifyFull', func.invoke(justifyFull, 'render'));
   5453       context.memo('button.outdent', func.invoke(outdent, 'render'));
   5454       context.memo('button.indent', func.invoke(indent, 'render'));
   5455 
   5456       context.memo('button.paragraph', function () {
   5457         return ui.buttonGroup([
   5458           ui.button({
   5459             className: 'dropdown-toggle',
   5460             contents: ui.icon(options.icons.alignLeft) + ' ' + ui.icon(options.icons.caret, 'span'),
   5461             tooltip: lang.paragraph.paragraph,
   5462             data: {
   5463               toggle: 'dropdown'
   5464             }
   5465           }),
   5466           ui.dropdown([
   5467             ui.buttonGroup({
   5468               className: 'note-align',
   5469               children: [justifyLeft, justifyCenter, justifyRight, justifyFull]
   5470             }),
   5471             ui.buttonGroup({
   5472               className: 'note-list',
   5473               children: [outdent, indent]
   5474             })
   5475           ])
   5476         ]).render();
   5477       });
   5478 
   5479       context.memo('button.height', function () {
   5480         return ui.buttonGroup([
   5481           ui.button({
   5482             className: 'dropdown-toggle',
   5483             contents: ui.icon(options.icons.textHeight) + ' ' + ui.icon(options.icons.caret, 'span'),
   5484             tooltip: lang.font.height,
   5485             data: {
   5486               toggle: 'dropdown'
   5487             }
   5488           }),
   5489           ui.dropdownCheck({
   5490             items: options.lineHeights,
   5491             checkClassName: options.icons.menuCheck,
   5492             className: 'dropdown-line-height',
   5493             click: context.createInvokeHandler('editor.lineHeight')
   5494           })
   5495         ]).render();
   5496       });
   5497 
   5498       context.memo('button.table', function () {
   5499         return ui.buttonGroup([
   5500           ui.button({
   5501             className: 'dropdown-toggle',
   5502             contents: ui.icon(options.icons.table) + ' ' + ui.icon(options.icons.caret, 'span'),
   5503             tooltip: lang.table.table,
   5504             data: {
   5505               toggle: 'dropdown'
   5506             }
   5507           }),
   5508           ui.dropdown({
   5509             className: 'note-table',
   5510             items: [
   5511               '<div class="note-dimension-picker">',
   5512               '  <div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"/>',
   5513               '  <div class="note-dimension-picker-highlighted"/>',
   5514               '  <div class="note-dimension-picker-unhighlighted"/>',
   5515               '</div>',
   5516               '<div class="note-dimension-display">1 x 1</div>'
   5517             ].join('')
   5518           })
   5519         ], {
   5520           callback: function ($node) {
   5521             var $catcher = $node.find('.note-dimension-picker-mousecatcher');
   5522             $catcher.css({
   5523               width: options.insertTableMaxSize.col + 'em',
   5524               height: options.insertTableMaxSize.row + 'em'
   5525             }).mousedown(context.createInvokeHandler('editor.insertTable'))
   5526               .on('mousemove', self.tableMoveHandler);
   5527           }
   5528         }).render();
   5529       });
   5530 
   5531       context.memo('button.link', function () {
   5532         return ui.button({
   5533           contents: ui.icon(options.icons.link),
   5534           tooltip: lang.link.link + representShortcut('linkDialog.show'),
   5535           click: context.createInvokeHandler('linkDialog.show')
   5536         }).render();
   5537       });
   5538 
   5539       context.memo('button.picture', function () {
   5540         return ui.button({
   5541           contents: ui.icon(options.icons.picture),
   5542           tooltip: lang.image.image,
   5543           click: context.createInvokeHandler('imageDialog.show')
   5544         }).render();
   5545       });
   5546 
   5547       context.memo('button.video', function () {
   5548         return ui.button({
   5549           contents: ui.icon(options.icons.video),
   5550           tooltip: lang.video.video,
   5551           click: context.createInvokeHandler('videoDialog.show')
   5552         }).render();
   5553       });
   5554 
   5555       context.memo('button.hr', function () {
   5556         return ui.button({
   5557           contents: ui.icon(options.icons.minus),
   5558           tooltip: lang.hr.insert + representShortcut('insertHorizontalRule'),
   5559           click: context.createInvokeHandler('editor.insertHorizontalRule')
   5560         }).render();
   5561       });
   5562 
   5563       context.memo('button.fullscreen', function () {
   5564         return ui.button({
   5565           className: 'btn-fullscreen',
   5566           contents: ui.icon(options.icons.arrowsAlt),
   5567           tooltip: lang.options.fullscreen,
   5568           click: context.createInvokeHandler('fullscreen.toggle')
   5569         }).render();
   5570       });
   5571 
   5572       context.memo('button.codeview', function () {
   5573         return ui.button({
   5574           className: 'btn-codeview',
   5575           contents: ui.icon(options.icons.code),
   5576           tooltip: lang.options.codeview,
   5577           click: context.createInvokeHandler('codeview.toggle')
   5578         }).render();
   5579       });
   5580 
   5581       context.memo('button.redo', function () {
   5582         return ui.button({
   5583           contents: ui.icon(options.icons.redo),
   5584           tooltip: lang.history.redo + representShortcut('redo'),
   5585           click: context.createInvokeHandler('editor.redo')
   5586         }).render();
   5587       });
   5588 
   5589       context.memo('button.undo', function () {
   5590         return ui.button({
   5591           contents: ui.icon(options.icons.undo),
   5592           tooltip: lang.history.undo + representShortcut('undo'),
   5593           click: context.createInvokeHandler('editor.undo')
   5594         }).render();
   5595       });
   5596 
   5597       context.memo('button.help', function () {
   5598         return ui.button({
   5599           contents: ui.icon(options.icons.question),
   5600           tooltip: lang.options.help,
   5601           click: context.createInvokeHandler('helpDialog.show')
   5602         }).render();
   5603       });
   5604     };
   5605 
   5606     /**
   5607      * image : [
   5608      *   ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
   5609      *   ['float', ['floatLeft', 'floatRight', 'floatNone' ]],
   5610      *   ['remove', ['removeMedia']]
   5611      * ],
   5612      */
   5613     this.addImagePopoverButtons = function () {
   5614       // Image Size Buttons
   5615       context.memo('button.imageSize100', function () {
   5616         return ui.button({
   5617           contents: '<span class="note-fontsize-10">100%</span>',
   5618           tooltip: lang.image.resizeFull,
   5619           click: context.createInvokeHandler('editor.resize', '1')
   5620         }).render();
   5621       });
   5622       context.memo('button.imageSize50', function () {
   5623         return  ui.button({
   5624           contents: '<span class="note-fontsize-10">50%</span>',
   5625           tooltip: lang.image.resizeHalf,
   5626           click: context.createInvokeHandler('editor.resize', '0.5')
   5627         }).render();
   5628       });
   5629       context.memo('button.imageSize25', function () {
   5630         return ui.button({
   5631           contents: '<span class="note-fontsize-10">25%</span>',
   5632           tooltip: lang.image.resizeQuarter,
   5633           click: context.createInvokeHandler('editor.resize', '0.25')
   5634         }).render();
   5635       });
   5636 
   5637       // Float Buttons
   5638       context.memo('button.floatLeft', function () {
   5639         return ui.button({
   5640           contents: ui.icon(options.icons.alignLeft),
   5641           tooltip: lang.image.floatLeft,
   5642           click: context.createInvokeHandler('editor.floatMe', 'left')
   5643         }).render();
   5644       });
   5645 
   5646       context.memo('button.floatRight', function () {
   5647         return ui.button({
   5648           contents: ui.icon(options.icons.alignRight),
   5649           tooltip: lang.image.floatRight,
   5650           click: context.createInvokeHandler('editor.floatMe', 'right')
   5651         }).render();
   5652       });
   5653 
   5654       context.memo('button.floatNone', function () {
   5655         return ui.button({
   5656           contents: ui.icon(options.icons.alignJustify),
   5657           tooltip: lang.image.floatNone,
   5658           click: context.createInvokeHandler('editor.floatMe', 'none')
   5659         }).render();
   5660       });
   5661 
   5662       // Remove Buttons
   5663       context.memo('button.removeMedia', function () {
   5664         return ui.button({
   5665           contents: ui.icon(options.icons.trash),
   5666           tooltip: lang.image.remove,
   5667           click: context.createInvokeHandler('editor.removeMedia')
   5668         }).render();
   5669       });
   5670     };
   5671 
   5672     this.addLinkPopoverButtons = function () {
   5673       context.memo('button.linkDialogShow', function () {
   5674         return ui.button({
   5675           contents: ui.icon(options.icons.link),
   5676           tooltip: lang.link.edit,
   5677           click: context.createInvokeHandler('linkDialog.show')
   5678         }).render();
   5679       });
   5680 
   5681       context.memo('button.unlink', function () {
   5682         return ui.button({
   5683           contents: ui.icon(options.icons.unlink),
   5684           tooltip: lang.link.unlink,
   5685           click: context.createInvokeHandler('editor.unlink')
   5686         }).render();
   5687       });
   5688     };
   5689 
   5690     this.build = function ($container, groups) {
   5691       for (var groupIdx = 0, groupLen = groups.length; groupIdx < groupLen; groupIdx++) {
   5692         var group = groups[groupIdx];
   5693         var groupName = group[0];
   5694         var buttons = group[1];
   5695 
   5696         var $group = ui.buttonGroup({
   5697           className: 'note-' + groupName
   5698         }).render();
   5699 
   5700         for (var idx = 0, len = buttons.length; idx < len; idx++) {
   5701           var button = context.memo('button.' + buttons[idx]);
   5702           if (button) {
   5703             $group.append(typeof button === 'function' ? button(context) : button);
   5704           }
   5705         }
   5706         $group.appendTo($container);
   5707       }
   5708     };
   5709 
   5710     this.updateCurrentStyle = function () {
   5711       var styleInfo = context.invoke('editor.currentStyle');
   5712       this.updateBtnStates({
   5713         '.note-btn-bold': function () {
   5714           return styleInfo['font-bold'] === 'bold';
   5715         },
   5716         '.note-btn-italic': function () {
   5717           return styleInfo['font-italic'] === 'italic';
   5718         },
   5719         '.note-btn-underline': function () {
   5720           return styleInfo['font-underline'] === 'underline';
   5721         },
   5722         '.note-btn-subscript': function () {
   5723           return styleInfo['font-subscript'] === 'subscript';
   5724         },
   5725         '.note-btn-superscript': function () {
   5726           return styleInfo['font-superscript'] === 'superscript';
   5727         },
   5728         '.note-btn-strikethrough': function () {
   5729           return styleInfo['font-strikethrough'] === 'strikethrough';
   5730         }
   5731       });
   5732 
   5733       if (styleInfo['font-family']) {
   5734         var fontNames = styleInfo['font-family'].split(',').map(function (name) {
   5735           return name.replace(/[\'\"]/g, '')
   5736             .replace(/\s+$/, '')
   5737             .replace(/^\s+/, '');
   5738         });
   5739         var fontName = list.find(fontNames, self.isFontInstalled);
   5740 
   5741         $toolbar.find('.dropdown-fontname li a').each(function () {
   5742           // always compare string to avoid creating another func.
   5743           var isChecked = ($(this).data('value') + '') === (fontName + '');
   5744           this.className = isChecked ? 'checked' : '';
   5745         });
   5746         $toolbar.find('.note-current-fontname').text(fontName);
   5747       }
   5748 
   5749       if (styleInfo['font-size']) {
   5750         var fontSize = styleInfo['font-size'];
   5751         $toolbar.find('.dropdown-fontsize li a').each(function () {
   5752           // always compare with string to avoid creating another func.
   5753           var isChecked = ($(this).data('value') + '') === (fontSize + '');
   5754           this.className = isChecked ? 'checked' : '';
   5755         });
   5756         $toolbar.find('.note-current-fontsize').text(fontSize);
   5757       }
   5758 
   5759       if (styleInfo['line-height']) {
   5760         var lineHeight = styleInfo['line-height'];
   5761         $toolbar.find('.dropdown-line-height li a').each(function () {
   5762           // always compare with string to avoid creating another func.
   5763           var isChecked = ($(this).data('value') + '') === (lineHeight + '');
   5764           this.className = isChecked ? 'checked' : '';
   5765         });
   5766       }
   5767     };
   5768 
   5769     this.updateBtnStates = function (infos) {
   5770       $.each(infos, function (selector, pred) {
   5771         ui.toggleBtnActive($toolbar.find(selector), pred());
   5772       });
   5773     };
   5774 
   5775     this.tableMoveHandler = function (event) {
   5776       var PX_PER_EM = 18;
   5777       var $picker = $(event.target.parentNode); // target is mousecatcher
   5778       var $dimensionDisplay = $picker.next();
   5779       var $catcher = $picker.find('.note-dimension-picker-mousecatcher');
   5780       var $highlighted = $picker.find('.note-dimension-picker-highlighted');
   5781       var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted');
   5782 
   5783       var posOffset;
   5784       // HTML5 with jQuery - e.offsetX is undefined in Firefox
   5785       if (event.offsetX === undefined) {
   5786         var posCatcher = $(event.target).offset();
   5787         posOffset = {
   5788           x: event.pageX - posCatcher.left,
   5789           y: event.pageY - posCatcher.top
   5790         };
   5791       } else {
   5792         posOffset = {
   5793           x: event.offsetX,
   5794           y: event.offsetY
   5795         };
   5796       }
   5797 
   5798       var dim = {
   5799         c: Math.ceil(posOffset.x / PX_PER_EM) || 1,
   5800         r: Math.ceil(posOffset.y / PX_PER_EM) || 1
   5801       };
   5802 
   5803       $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' });
   5804       $catcher.data('value', dim.c + 'x' + dim.r);
   5805 
   5806       if (3 < dim.c && dim.c < options.insertTableMaxSize.col) {
   5807         $unhighlighted.css({ width: dim.c + 1 + 'em'});
   5808       }
   5809 
   5810       if (3 < dim.r && dim.r < options.insertTableMaxSize.row) {
   5811         $unhighlighted.css({ height: dim.r + 1 + 'em'});
   5812       }
   5813 
   5814       $dimensionDisplay.html(dim.c + ' x ' + dim.r);
   5815     };
   5816   };
   5817 
   5818   var Toolbar = function (context) {
   5819     var ui = $.summernote.ui;
   5820 
   5821     var $note = context.layoutInfo.note;
   5822     var $toolbar = context.layoutInfo.toolbar;
   5823     var options = context.options;
   5824 
   5825     this.shouldInitialize = function () {
   5826       return !options.airMode;
   5827     };
   5828 
   5829     this.initialize = function () {
   5830       options.toolbar = options.toolbar || [];
   5831 
   5832       if (!options.toolbar.length) {
   5833         $toolbar.hide();
   5834       } else {
   5835         context.invoke('buttons.build', $toolbar, options.toolbar);
   5836       }
   5837 
   5838       if (options.toolbarContainer) {
   5839         $toolbar.appendTo(options.toolbarContainer);
   5840       }
   5841 
   5842       $note.on('summernote.keyup summernote.mouseup summernote.change', function () {
   5843         context.invoke('buttons.updateCurrentStyle');
   5844       });
   5845 
   5846       context.invoke('buttons.updateCurrentStyle');
   5847     };
   5848 
   5849     this.destroy = function () {
   5850       $toolbar.children().remove();
   5851     };
   5852 
   5853     this.updateFullscreen = function (isFullscreen) {
   5854       ui.toggleBtnActive($toolbar.find('.btn-fullscreen'), isFullscreen);
   5855     };
   5856 
   5857     this.updateCodeview = function (isCodeview) {
   5858       ui.toggleBtnActive($toolbar.find('.btn-codeview'), isCodeview);
   5859       if (isCodeview) {
   5860         this.deactivate();
   5861       } else {
   5862         this.activate();
   5863       }
   5864     };
   5865 
   5866     this.activate = function (isIncludeCodeview) {
   5867       var $btn = $toolbar.find('button');
   5868       if (!isIncludeCodeview) {
   5869         $btn = $btn.not('.btn-codeview');
   5870       }
   5871       ui.toggleBtn($btn, true);
   5872     };
   5873 
   5874     this.deactivate = function (isIncludeCodeview) {
   5875       var $btn = $toolbar.find('button');
   5876       if (!isIncludeCodeview) {
   5877         $btn = $btn.not('.btn-codeview');
   5878       }
   5879       ui.toggleBtn($btn, false);
   5880     };
   5881   };
   5882 
   5883   var LinkDialog = function (context) {
   5884     var self = this;
   5885     var ui = $.summernote.ui;
   5886 
   5887     var $editor = context.layoutInfo.editor;
   5888     var options = context.options;
   5889     var lang = options.langInfo;
   5890 
   5891     this.initialize = function () {
   5892       var $container = options.dialogsInBody ? $(document.body) : $editor;
   5893 
   5894       var body = '<div class="form-group">' +
   5895                    '<label>' + lang.link.textToDisplay + '</label>' +
   5896                    '<input class="note-link-text form-control" type="text" />' +
   5897                  '</div>' +
   5898                  '<div class="form-group">' +
   5899                    '<label>' + lang.link.url + '</label>' +
   5900                    '<input class="note-link-url form-control" type="text" value="http://" />' +
   5901                  '</div>' +
   5902                  (!options.disableLinkTarget ?
   5903                    '<div class="checkbox">' +
   5904                      '<label>' + '<input type="checkbox" checked> ' + lang.link.openInNewWindow + '</label>' +
   5905                    '</div>' : ''
   5906                  );
   5907       var footer = '<button href="#" class="btn btn-primary note-link-btn disabled" disabled>' + lang.link.insert + '</button>';
   5908 
   5909       this.$dialog = ui.dialog({
   5910         className: 'link-dialog',
   5911         title: lang.link.insert,
   5912         fade: options.dialogsFade,
   5913         body: body,
   5914         footer: footer
   5915       }).render().appendTo($container);
   5916     };
   5917 
   5918     this.destroy = function () {
   5919       ui.hideDialog(this.$dialog);
   5920       this.$dialog.remove();
   5921     };
   5922 
   5923     this.bindEnterKey = function ($input, $btn) {
   5924       $input.on('keypress', function (event) {
   5925         if (event.keyCode === key.code.ENTER) {
   5926           $btn.trigger('click');
   5927         }
   5928       });
   5929     };
   5930 
   5931     /**
   5932      * toggle update button
   5933      */
   5934     this.toggleLinkBtn = function ($linkBtn, $linkText, $linkUrl) {
   5935       ui.toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
   5936     };
   5937 
   5938     /**
   5939      * Show link dialog and set event handlers on dialog controls.
   5940      *
   5941      * @param {Object} linkInfo
   5942      * @return {Promise}
   5943      */
   5944     this.showLinkDialog = function (linkInfo) {
   5945       return $.Deferred(function (deferred) {
   5946         var $linkText = self.$dialog.find('.note-link-text'),
   5947         $linkUrl = self.$dialog.find('.note-link-url'),
   5948         $linkBtn = self.$dialog.find('.note-link-btn'),
   5949         $openInNewWindow = self.$dialog.find('input[type=checkbox]');
   5950 
   5951         ui.onDialogShown(self.$dialog, function () {
   5952           context.triggerEvent('dialog.shown');
   5953 
   5954           // if no url was given, copy text to url
   5955           if (!linkInfo.url) {
   5956             linkInfo.url = linkInfo.text;
   5957           }
   5958 
   5959           $linkText.val(linkInfo.text);
   5960 
   5961           var handleLinkTextUpdate = function () {
   5962             self.toggleLinkBtn($linkBtn, $linkText, $linkUrl);
   5963             // if linktext was modified by keyup,
   5964             // stop cloning text from linkUrl
   5965             linkInfo.text = $linkText.val();
   5966           };
   5967 
   5968           $linkText.on('input', handleLinkTextUpdate).on('paste', function () {
   5969             setTimeout(handleLinkTextUpdate, 0);
   5970           });
   5971 
   5972           var handleLinkUrlUpdate = function () {
   5973             self.toggleLinkBtn($linkBtn, $linkText, $linkUrl);
   5974             // display same link on `Text to display` input
   5975             // when create a new link
   5976             if (!linkInfo.text) {
   5977               $linkText.val($linkUrl.val());
   5978             }
   5979           };
   5980 
   5981           $linkUrl.on('input', handleLinkUrlUpdate).on('paste', function () {
   5982             setTimeout(handleLinkUrlUpdate, 0);
   5983           }).val(linkInfo.url).trigger('focus');
   5984 
   5985           self.toggleLinkBtn($linkBtn, $linkText, $linkUrl);
   5986           self.bindEnterKey($linkUrl, $linkBtn);
   5987           self.bindEnterKey($linkText, $linkBtn);
   5988 
   5989           $openInNewWindow.prop('checked', linkInfo.isNewWindow);
   5990 
   5991           $linkBtn.one('click', function (event) {
   5992             event.preventDefault();
   5993 
   5994             deferred.resolve({
   5995               range: linkInfo.range,
   5996               url: $linkUrl.val(),
   5997               text: $linkText.val(),
   5998               isNewWindow: $openInNewWindow.is(':checked')
   5999             });
   6000             self.$dialog.modal('hide');
   6001           });
   6002         });
   6003 
   6004         ui.onDialogHidden(self.$dialog, function () {
   6005           // detach events
   6006           $linkText.off('input paste keypress');
   6007           $linkUrl.off('input paste keypress');
   6008           $linkBtn.off('click');
   6009 
   6010           if (deferred.state() === 'pending') {
   6011             deferred.reject();
   6012           }
   6013         });
   6014 
   6015         ui.showDialog(self.$dialog);
   6016       }).promise();
   6017     };
   6018 
   6019     /**
   6020      * @param {Object} layoutInfo
   6021      */
   6022     this.show = function () {
   6023       var linkInfo = context.invoke('editor.getLinkInfo');
   6024 
   6025       context.invoke('editor.saveRange');
   6026       this.showLinkDialog(linkInfo).then(function (linkInfo) {
   6027         context.invoke('editor.restoreRange');
   6028         context.invoke('editor.createLink', linkInfo);
   6029       }).fail(function () {
   6030         context.invoke('editor.restoreRange');
   6031       });
   6032     };
   6033     context.memo('help.linkDialog.show', options.langInfo.help['linkDialog.show']);
   6034   };
   6035 
   6036   var LinkPopover = function (context) {
   6037     var self = this;
   6038     var ui = $.summernote.ui;
   6039 
   6040     var options = context.options;
   6041 
   6042     this.events = {
   6043       'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function () {
   6044         self.update();
   6045       },
   6046       'summernote.dialog.shown': function () {
   6047         self.hide();
   6048       }
   6049     };
   6050 
   6051     this.shouldInitialize = function () {
   6052       return !list.isEmpty(options.popover.link);
   6053     };
   6054 
   6055     this.initialize = function () {
   6056       this.$popover = ui.popover({
   6057         className: 'note-link-popover',
   6058         callback: function ($node) {
   6059           var $content = $node.find('.popover-content');
   6060           $content.prepend('<span><a target="_blank"></a>&nbsp;</span>');
   6061         }
   6062       }).render().appendTo('body');
   6063       var $content = this.$popover.find('.popover-content');
   6064 
   6065       context.invoke('buttons.build', $content, options.popover.link);
   6066     };
   6067 
   6068     this.destroy = function () {
   6069       this.$popover.remove();
   6070     };
   6071 
   6072     this.update = function () {
   6073       // Prevent focusing on editable when invoke('code') is executed
   6074       if (!context.invoke('editor.hasFocus')) {
   6075         this.hide();
   6076         return;
   6077       }
   6078 
   6079       var rng = context.invoke('editor.createRange');
   6080       if (rng.isCollapsed() && rng.isOnAnchor()) {
   6081         var anchor = dom.ancestor(rng.sc, dom.isAnchor);
   6082         var href = $(anchor).attr('href');
   6083         this.$popover.find('a').attr('href', href).html(href);
   6084 
   6085         var pos = dom.posFromPlaceholder(anchor);
   6086         this.$popover.css({
   6087           display: 'block',
   6088           left: pos.left,
   6089           top: pos.top
   6090         });
   6091       } else {
   6092         this.hide();
   6093       }
   6094     };
   6095 
   6096     this.hide = function () {
   6097       this.$popover.hide();
   6098     };
   6099   };
   6100 
   6101   var ImageDialog = function (context) {
   6102     var self = this;
   6103     var ui = $.summernote.ui;
   6104 
   6105     var $editor = context.layoutInfo.editor;
   6106     var options = context.options;
   6107     var lang = options.langInfo;
   6108 
   6109     this.initialize = function () {
   6110       var $container = options.dialogsInBody ? $(document.body) : $editor;
   6111 
   6112       var imageLimitation = '';
   6113       if (options.maximumImageFileSize) {
   6114         var unit = Math.floor(Math.log(options.maximumImageFileSize) / Math.log(1024));
   6115         var readableSize = (options.maximumImageFileSize / Math.pow(1024, unit)).toFixed(2) * 1 +
   6116                            ' ' + ' KMGTP'[unit] + 'B';
   6117         imageLimitation = '<small>' + lang.image.maximumFileSize + ' : ' + readableSize + '</small>';
   6118       }
   6119 
   6120       var body = '<div class="form-group note-group-select-from-files">' +
   6121                    '<label>' + lang.image.selectFromFiles + '</label>' +
   6122                    '<input class="note-image-input form-control" type="file" name="files" accept="image/*" multiple="multiple" />' +
   6123                    imageLimitation +
   6124                  '</div>' +
   6125                  '<div class="form-group note-group-image-url" style="overflow:auto;">' +
   6126                    '<label>' + lang.image.url + '</label>' +
   6127                    '<input class="note-image-url form-control col-md-12" type="text" />' +
   6128                  '</div>';
   6129       var footer = '<button href="#" class="btn btn-primary note-image-btn disabled" disabled>' + lang.image.insert + '</button>';
   6130 
   6131       this.$dialog = ui.dialog({
   6132         title: lang.image.insert,
   6133         fade: options.dialogsFade,
   6134         body: body,
   6135         footer: footer
   6136       }).render().appendTo($container);
   6137     };
   6138 
   6139     this.destroy = function () {
   6140       ui.hideDialog(this.$dialog);
   6141       this.$dialog.remove();
   6142     };
   6143 
   6144     this.bindEnterKey = function ($input, $btn) {
   6145       $input.on('keypress', function (event) {
   6146         if (event.keyCode === key.code.ENTER) {
   6147           $btn.trigger('click');
   6148         }
   6149       });
   6150     };
   6151 
   6152     this.show = function () {
   6153       context.invoke('editor.saveRange');
   6154       this.showImageDialog().then(function (data) {
   6155         // [workaround] hide dialog before restore range for IE range focus
   6156         ui.hideDialog(self.$dialog);
   6157         context.invoke('editor.restoreRange');
   6158 
   6159         if (typeof data === 'string') { // image url
   6160           context.invoke('editor.insertImage', data);
   6161         } else { // array of files
   6162           context.invoke('editor.insertImagesOrCallback', data);
   6163         }
   6164       }).fail(function () {
   6165         context.invoke('editor.restoreRange');
   6166       });
   6167     };
   6168 
   6169     /**
   6170      * show image dialog
   6171      *
   6172      * @param {jQuery} $dialog
   6173      * @return {Promise}
   6174      */
   6175     this.showImageDialog = function () {
   6176       return $.Deferred(function (deferred) {
   6177         var $imageInput = self.$dialog.find('.note-image-input'),
   6178             $imageUrl = self.$dialog.find('.note-image-url'),
   6179             $imageBtn = self.$dialog.find('.note-image-btn');
   6180 
   6181         ui.onDialogShown(self.$dialog, function () {
   6182           context.triggerEvent('dialog.shown');
   6183 
   6184           // Cloning imageInput to clear element.
   6185           $imageInput.replaceWith($imageInput.clone()
   6186             .on('change', function () {
   6187               deferred.resolve(this.files || this.value);
   6188             })
   6189             .val('')
   6190           );
   6191 
   6192           $imageBtn.click(function (event) {
   6193             event.preventDefault();
   6194 
   6195             deferred.resolve($imageUrl.val());
   6196           });
   6197 
   6198           $imageUrl.on('keyup paste', function () {
   6199             var url = $imageUrl.val();
   6200             ui.toggleBtn($imageBtn, url);
   6201           }).val('').trigger('focus');
   6202           self.bindEnterKey($imageUrl, $imageBtn);
   6203         });
   6204 
   6205         ui.onDialogHidden(self.$dialog, function () {
   6206           $imageInput.off('change');
   6207           $imageUrl.off('keyup paste keypress');
   6208           $imageBtn.off('click');
   6209 
   6210           if (deferred.state() === 'pending') {
   6211             deferred.reject();
   6212           }
   6213         });
   6214 
   6215         ui.showDialog(self.$dialog);
   6216       });
   6217     };
   6218   };
   6219 
   6220   var ImagePopover = function (context) {
   6221     var ui = $.summernote.ui;
   6222 
   6223     var options = context.options;
   6224 
   6225     this.shouldInitialize = function () {
   6226       return !list.isEmpty(options.popover.image);
   6227     };
   6228 
   6229     this.initialize = function () {
   6230       this.$popover = ui.popover({
   6231         className: 'note-image-popover'
   6232       }).render().appendTo('body');
   6233       var $content = this.$popover.find('.popover-content');
   6234 
   6235       context.invoke('buttons.build', $content, options.popover.image);
   6236     };
   6237 
   6238     this.destroy = function () {
   6239       this.$popover.remove();
   6240     };
   6241 
   6242     this.update = function (target) {
   6243       if (dom.isImg(target)) {
   6244         var pos = dom.posFromPlaceholder(target);
   6245         this.$popover.css({
   6246           display: 'block',
   6247           left: pos.left,
   6248           top: pos.top
   6249         });
   6250       } else {
   6251         this.hide();
   6252       }
   6253     };
   6254 
   6255     this.hide = function () {
   6256       this.$popover.hide();
   6257     };
   6258   };
   6259 
   6260   var VideoDialog = function (context) {
   6261     var self = this;
   6262     var ui = $.summernote.ui;
   6263 
   6264     var $editor = context.layoutInfo.editor;
   6265     var options = context.options;
   6266     var lang = options.langInfo;
   6267 
   6268     this.initialize = function () {
   6269       var $container = options.dialogsInBody ? $(document.body) : $editor;
   6270 
   6271       var body = '<div class="form-group row-fluid">' +
   6272           '<label>' + lang.video.url + ' <small class="text-muted">' + lang.video.providers + '</small></label>' +
   6273           '<input class="note-video-url form-control span12" type="text" />' +
   6274           '</div>';
   6275       var footer = '<button href="#" class="btn btn-primary note-video-btn disabled" disabled>' + lang.video.insert + '</button>';
   6276 
   6277       this.$dialog = ui.dialog({
   6278         title: lang.video.insert,
   6279         fade: options.dialogsFade,
   6280         body: body,
   6281         footer: footer
   6282       }).render().appendTo($container);
   6283     };
   6284 
   6285     this.destroy = function () {
   6286       ui.hideDialog(this.$dialog);
   6287       this.$dialog.remove();
   6288     };
   6289 
   6290     this.bindEnterKey = function ($input, $btn) {
   6291       $input.on('keypress', function (event) {
   6292         if (event.keyCode === key.code.ENTER) {
   6293           $btn.trigger('click');
   6294         }
   6295       });
   6296     };
   6297 
   6298     this.createVideoNode = function (url) {
   6299       // video url patterns(youtube, instagram, vimeo, dailymotion, youku, mp4, ogg, webm)
   6300       var ytRegExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
   6301       var ytMatch = url.match(ytRegExp);
   6302 
   6303       var igRegExp = /(?:www\.|\/\/)instagram\.com\/p\/(.[a-zA-Z0-9_-]*)/;
   6304       var igMatch = url.match(igRegExp);
   6305 
   6306       var vRegExp = /\/\/vine\.co\/v\/([a-zA-Z0-9]+)/;
   6307       var vMatch = url.match(vRegExp);
   6308 
   6309       var vimRegExp = /\/\/(player\.)?vimeo\.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
   6310       var vimMatch = url.match(vimRegExp);
   6311 
   6312       var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
   6313       var dmMatch = url.match(dmRegExp);
   6314 
   6315       var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/;
   6316       var youkuMatch = url.match(youkuRegExp);
   6317 
   6318       var mp4RegExp = /^.+.(mp4|m4v)$/;
   6319       var mp4Match = url.match(mp4RegExp);
   6320 
   6321       var oggRegExp = /^.+.(ogg|ogv)$/;
   6322       var oggMatch = url.match(oggRegExp);
   6323 
   6324       var webmRegExp = /^.+.(webm)$/;
   6325       var webmMatch = url.match(webmRegExp);
   6326 
   6327       var $video;
   6328       if (ytMatch && ytMatch[1].length === 11) {
   6329         var youtubeId = ytMatch[1];
   6330         $video = $('<iframe>')
   6331             .attr('frameborder', 0)
   6332             .attr('src', '//www.youtube.com/embed/' + youtubeId)
   6333             .attr('width', '640').attr('height', '360');
   6334       } else if (igMatch && igMatch[0].length) {
   6335         $video = $('<iframe>')
   6336             .attr('frameborder', 0)
   6337             .attr('src', 'https://instagram.com/p/' + igMatch[1] + '/embed/')
   6338             .attr('width', '612').attr('height', '710')
   6339             .attr('scrolling', 'no')
   6340             .attr('allowtransparency', 'true');
   6341       } else if (vMatch && vMatch[0].length) {
   6342         $video = $('<iframe>')
   6343             .attr('frameborder', 0)
   6344             .attr('src', vMatch[0] + '/embed/simple')
   6345             .attr('width', '600').attr('height', '600')
   6346             .attr('class', 'vine-embed');
   6347       } else if (vimMatch && vimMatch[3].length) {
   6348         $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
   6349             .attr('frameborder', 0)
   6350             .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
   6351             .attr('width', '640').attr('height', '360');
   6352       } else if (dmMatch && dmMatch[2].length) {
   6353         $video = $('<iframe>')
   6354             .attr('frameborder', 0)
   6355             .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
   6356             .attr('width', '640').attr('height', '360');
   6357       } else if (youkuMatch && youkuMatch[1].length) {
   6358         $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
   6359             .attr('frameborder', 0)
   6360             .attr('height', '498')
   6361             .attr('width', '510')
   6362             .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
   6363       } else if (mp4Match || oggMatch || webmMatch) {
   6364         $video = $('<video controls>')
   6365             .attr('src', url)
   6366             .attr('width', '640').attr('height', '360');
   6367       } else {
   6368         // this is not a known video link. Now what, Cat? Now what?
   6369         return false;
   6370       }
   6371 
   6372       $video.addClass('note-video-clip');
   6373 
   6374       return $video[0];
   6375     };
   6376 
   6377     this.show = function () {
   6378       var text = context.invoke('editor.getSelectedText');
   6379       context.invoke('editor.saveRange');
   6380       this.showVideoDialog(text).then(function (url) {
   6381         // [workaround] hide dialog before restore range for IE range focus
   6382         ui.hideDialog(self.$dialog);
   6383         context.invoke('editor.restoreRange');
   6384 
   6385         // build node
   6386         var $node = self.createVideoNode(url);
   6387 
   6388         if ($node) {
   6389           // insert video node
   6390           context.invoke('editor.insertNode', $node);
   6391         }
   6392       }).fail(function () {
   6393         context.invoke('editor.restoreRange');
   6394       });
   6395     };
   6396 
   6397     /**
   6398      * show image dialog
   6399      *
   6400      * @param {jQuery} $dialog
   6401      * @return {Promise}
   6402      */
   6403     this.showVideoDialog = function (text) {
   6404       return $.Deferred(function (deferred) {
   6405         var $videoUrl = self.$dialog.find('.note-video-url'),
   6406             $videoBtn = self.$dialog.find('.note-video-btn');
   6407 
   6408         ui.onDialogShown(self.$dialog, function () {
   6409           context.triggerEvent('dialog.shown');
   6410 
   6411           $videoUrl.val(text).on('input', function () {
   6412             ui.toggleBtn($videoBtn, $videoUrl.val());
   6413           }).trigger('focus');
   6414 
   6415           $videoBtn.click(function (event) {
   6416             event.preventDefault();
   6417 
   6418             deferred.resolve($videoUrl.val());
   6419           });
   6420 
   6421           self.bindEnterKey($videoUrl, $videoBtn);
   6422         });
   6423 
   6424         ui.onDialogHidden(self.$dialog, function () {
   6425           $videoUrl.off('input');
   6426           $videoBtn.off('click');
   6427 
   6428           if (deferred.state() === 'pending') {
   6429             deferred.reject();
   6430           }
   6431         });
   6432 
   6433         ui.showDialog(self.$dialog);
   6434       });
   6435     };
   6436   };
   6437 
   6438   var HelpDialog = function (context) {
   6439     var self = this;
   6440     var ui = $.summernote.ui;
   6441 
   6442     var $editor = context.layoutInfo.editor;
   6443     var options = context.options;
   6444     var lang = options.langInfo;
   6445 
   6446     this.createShortCutList = function () {
   6447       var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
   6448       return Object.keys(keyMap).map(function (key) {
   6449         var command = keyMap[key];
   6450         var $row = $('<div><div class="help-list-item"/></div>');
   6451         $row.append($('<label><kbd>' + key + '</kdb></label>').css({
   6452           'width': 180,
   6453           'margin-right': 10
   6454         })).append($('<span/>').html(context.memo('help.' + command) || command));
   6455         return $row.html();
   6456       }).join('');
   6457     };
   6458 
   6459     this.initialize = function () {
   6460       var $container = options.dialogsInBody ? $(document.body) : $editor;
   6461 
   6462       var body = [
   6463         '<p class="text-center">',
   6464         '<a href="http://summernote.org/" target="_blank">Summernote 0.8.2</a> · ',
   6465         '<a href="https://github.com/summernote/summernote" target="_blank">Project</a> · ',
   6466         '<a href="https://github.com/summernote/summernote/issues" target="_blank">Issues</a>',
   6467         '</p>'
   6468       ].join('');
   6469 
   6470       this.$dialog = ui.dialog({
   6471         title: lang.options.help,
   6472         fade: options.dialogsFade,
   6473         body: this.createShortCutList(),
   6474         footer: body,
   6475         callback: function ($node) {
   6476           $node.find('.modal-body').css({
   6477             'max-height': 300,
   6478             'overflow': 'scroll'
   6479           });
   6480         }
   6481       }).render().appendTo($container);
   6482     };
   6483 
   6484     this.destroy = function () {
   6485       ui.hideDialog(this.$dialog);
   6486       this.$dialog.remove();
   6487     };
   6488 
   6489     /**
   6490      * show help dialog
   6491      *
   6492      * @return {Promise}
   6493      */
   6494     this.showHelpDialog = function () {
   6495       return $.Deferred(function (deferred) {
   6496         ui.onDialogShown(self.$dialog, function () {
   6497           context.triggerEvent('dialog.shown');
   6498           deferred.resolve();
   6499         });
   6500         ui.showDialog(self.$dialog);
   6501       }).promise();
   6502     };
   6503 
   6504     this.show = function () {
   6505       context.invoke('editor.saveRange');
   6506       this.showHelpDialog().then(function () {
   6507         context.invoke('editor.restoreRange');
   6508       });
   6509     };
   6510   };
   6511 
   6512   var AirPopover = function (context) {
   6513     var self = this;
   6514     var ui = $.summernote.ui;
   6515 
   6516     var options = context.options;
   6517 
   6518     var AIR_MODE_POPOVER_X_OFFSET = 20;
   6519 
   6520     this.events = {
   6521       'summernote.keyup summernote.mouseup summernote.scroll': function () {
   6522         self.update();
   6523       },
   6524       'summernote.change summernote.dialog.shown': function () {
   6525         self.hide();
   6526       },
   6527       'summernote.focusout': function (we, e) {
   6528         // [workaround] Firefox doesn't support relatedTarget on focusout
   6529         //  - Ignore hide action on focus out in FF.
   6530         if (agent.isFF) {
   6531           return;
   6532         }
   6533 
   6534         if (!e.relatedTarget || !dom.ancestor(e.relatedTarget, func.eq(self.$popover[0]))) {
   6535           self.hide();
   6536         }
   6537       }
   6538     };
   6539 
   6540     this.shouldInitialize = function () {
   6541       return options.airMode && !list.isEmpty(options.popover.air);
   6542     };
   6543 
   6544     this.initialize = function () {
   6545       this.$popover = ui.popover({
   6546         className: 'note-air-popover'
   6547       }).render().appendTo('body');
   6548       var $content = this.$popover.find('.popover-content');
   6549 
   6550       context.invoke('buttons.build', $content, options.popover.air);
   6551     };
   6552 
   6553     this.destroy = function () {
   6554       this.$popover.remove();
   6555     };
   6556 
   6557     this.update = function () {
   6558       var styleInfo = context.invoke('editor.currentStyle');
   6559       if (styleInfo.range && !styleInfo.range.isCollapsed()) {
   6560         var rect = list.last(styleInfo.range.getClientRects());
   6561         if (rect) {
   6562           var bnd = func.rect2bnd(rect);
   6563           this.$popover.css({
   6564             display: 'block',
   6565             left: Math.max(bnd.left + bnd.width / 2, 0) - AIR_MODE_POPOVER_X_OFFSET,
   6566             top: bnd.top + bnd.height
   6567           });
   6568         }
   6569       } else {
   6570         this.hide();
   6571       }
   6572     };
   6573 
   6574     this.hide = function () {
   6575       this.$popover.hide();
   6576     };
   6577   };
   6578 
   6579   var HintPopover = function (context) {
   6580     var self = this;
   6581     var ui = $.summernote.ui;
   6582 
   6583     var POPOVER_DIST = 5;
   6584     var hint = context.options.hint || [];
   6585     var direction = context.options.hintDirection || 'bottom';
   6586     var hints = $.isArray(hint) ? hint : [hint];
   6587 
   6588     this.events = {
   6589       'summernote.keyup': function (we, e) {
   6590         if (!e.isDefaultPrevented()) {
   6591           self.handleKeyup(e);
   6592         }
   6593       },
   6594       'summernote.keydown': function (we, e) {
   6595         self.handleKeydown(e);
   6596       },
   6597       'summernote.dialog.shown': function () {
   6598         self.hide();
   6599       }
   6600     };
   6601 
   6602     this.shouldInitialize = function () {
   6603       return hints.length > 0;
   6604     };
   6605 
   6606     this.initialize = function () {
   6607       this.lastWordRange = null;
   6608       this.$popover = ui.popover({
   6609         className: 'note-hint-popover',
   6610         hideArrow: true,
   6611         direction: ''
   6612       }).render().appendTo('body');
   6613 
   6614       this.$popover.hide();
   6615 
   6616       this.$content = this.$popover.find('.popover-content');
   6617 
   6618       this.$content.on('click', '.note-hint-item', function () {
   6619         self.$content.find('.active').removeClass('active');
   6620         $(this).addClass('active');
   6621         self.replace();
   6622       });
   6623     };
   6624 
   6625     this.destroy = function () {
   6626       this.$popover.remove();
   6627     };
   6628 
   6629     this.selectItem = function ($item) {
   6630       this.$content.find('.active').removeClass('active');
   6631       $item.addClass('active');
   6632 
   6633       this.$content[0].scrollTop = $item[0].offsetTop - (this.$content.innerHeight() / 2);
   6634     };
   6635 
   6636     this.moveDown = function () {
   6637       var $current = this.$content.find('.note-hint-item.active');
   6638       var $next = $current.next();
   6639 
   6640       if ($next.length) {
   6641         this.selectItem($next);
   6642       } else {
   6643         var $nextGroup = $current.parent().next();
   6644 
   6645         if (!$nextGroup.length) {
   6646           $nextGroup = this.$content.find('.note-hint-group').first();
   6647         }
   6648 
   6649         this.selectItem($nextGroup.find('.note-hint-item').first());
   6650       }
   6651     };
   6652 
   6653     this.moveUp = function () {
   6654       var $current = this.$content.find('.note-hint-item.active');
   6655       var $prev = $current.prev();
   6656 
   6657       if ($prev.length) {
   6658         this.selectItem($prev);
   6659       } else {
   6660         var $prevGroup = $current.parent().prev();
   6661 
   6662         if (!$prevGroup.length) {
   6663           $prevGroup = this.$content.find('.note-hint-group').last();
   6664         }
   6665 
   6666         this.selectItem($prevGroup.find('.note-hint-item').last());
   6667       }
   6668     };
   6669 
   6670     this.replace = function () {
   6671       var $item = this.$content.find('.note-hint-item.active');
   6672 
   6673       if ($item.length) {
   6674         var node = this.nodeFromItem($item);
   6675         this.lastWordRange.insertNode(node);
   6676         range.createFromNode(node).collapse().select();
   6677 
   6678         this.lastWordRange = null;
   6679         this.hide();
   6680         context.invoke('editor.focus');
   6681       }
   6682 
   6683     };
   6684 
   6685     this.nodeFromItem = function ($item) {
   6686       var hint = hints[$item.data('index')];
   6687       var item = $item.data('item');
   6688       var node = hint.content ? hint.content(item) : item;
   6689       if (typeof node === 'string') {
   6690         node = dom.createText(node);
   6691       }
   6692       return node;
   6693     };
   6694 
   6695     this.createItemTemplates = function (hintIdx, items) {
   6696       var hint = hints[hintIdx];
   6697       return items.map(function (item, idx) {
   6698         var $item = $('<div class="note-hint-item"/>');
   6699         $item.append(hint.template ? hint.template(item) : item + '');
   6700         $item.data({
   6701           'index': hintIdx,
   6702           'item': item
   6703         });
   6704 
   6705         if (hintIdx === 0 && idx === 0) {
   6706           $item.addClass('active');
   6707         }
   6708         return $item;
   6709       });
   6710     };
   6711 
   6712     this.handleKeydown = function (e) {
   6713       if (!this.$popover.is(':visible')) {
   6714         return;
   6715       }
   6716 
   6717       if (e.keyCode === key.code.ENTER) {
   6718         e.preventDefault();
   6719         this.replace();
   6720       } else if (e.keyCode === key.code.UP) {
   6721         e.preventDefault();
   6722         this.moveUp();
   6723       } else if (e.keyCode === key.code.DOWN) {
   6724         e.preventDefault();
   6725         this.moveDown();
   6726       }
   6727     };
   6728 
   6729     this.searchKeyword = function (index, keyword, callback) {
   6730       var hint = hints[index];
   6731       if (hint && hint.match.test(keyword) && hint.search) {
   6732         var matches = hint.match.exec(keyword);
   6733         hint.search(matches[1], callback);
   6734       } else {
   6735         callback();
   6736       }
   6737     };
   6738 
   6739     this.createGroup = function (idx, keyword) {
   6740       var $group = $('<div class="note-hint-group note-hint-group-' + idx + '"/>');
   6741       this.searchKeyword(idx, keyword, function (items) {
   6742         items = items || [];
   6743         if (items.length) {
   6744           $group.html(self.createItemTemplates(idx, items));
   6745           self.show();
   6746         }
   6747       });
   6748 
   6749       return $group;
   6750     };
   6751 
   6752     this.handleKeyup = function (e) {
   6753       if (list.contains([key.code.ENTER, key.code.UP, key.code.DOWN], e.keyCode)) {
   6754         if (e.keyCode === key.code.ENTER) {
   6755           if (this.$popover.is(':visible')) {
   6756             return;
   6757           }
   6758         }
   6759       } else {
   6760         var wordRange = context.invoke('editor.createRange').getWordRange();
   6761         var keyword = wordRange.toString();
   6762         if (hints.length && keyword) {
   6763           this.$content.empty();
   6764 
   6765           var bnd = func.rect2bnd(list.last(wordRange.getClientRects()));
   6766           if (bnd) {
   6767 
   6768             this.$popover.hide();
   6769 
   6770             this.lastWordRange = wordRange;
   6771 
   6772             hints.forEach(function (hint, idx) {
   6773               if (hint.match.test(keyword)) {
   6774                 self.createGroup(idx, keyword).appendTo(self.$content);
   6775               }
   6776             });
   6777 
   6778             // set position for popover after group is created
   6779             if (direction === 'top') {
   6780               this.$popover.css({
   6781                 left: bnd.left,
   6782                 top: bnd.top - this.$popover.outerHeight() - POPOVER_DIST
   6783               });
   6784             } else {
   6785               this.$popover.css({
   6786                 left: bnd.left,
   6787                 top: bnd.top + bnd.height + POPOVER_DIST
   6788               });
   6789             }
   6790 
   6791           }
   6792         } else {
   6793           this.hide();
   6794         }
   6795       }
   6796     };
   6797 
   6798     this.show = function () {
   6799       this.$popover.show();
   6800     };
   6801 
   6802     this.hide = function () {
   6803       this.$popover.hide();
   6804     };
   6805   };
   6806 
   6807 
   6808   $.summernote = $.extend($.summernote, {
   6809     version: '0.8.2',
   6810     ui: ui,
   6811     dom: dom,
   6812 
   6813     plugins: {},
   6814 
   6815     options: {
   6816       modules: {
   6817         'editor': Editor,
   6818         'clipboard': Clipboard,
   6819         'dropzone': Dropzone,
   6820         'codeview': Codeview,
   6821         'statusbar': Statusbar,
   6822         'fullscreen': Fullscreen,
   6823         'handle': Handle,
   6824         // FIXME: HintPopover must be front of autolink
   6825         //  - Script error about range when Enter key is pressed on hint popover
   6826         'hintPopover': HintPopover,
   6827         'autoLink': AutoLink,
   6828         'autoSync': AutoSync,
   6829         'placeholder': Placeholder,
   6830         'buttons': Buttons,
   6831         'toolbar': Toolbar,
   6832         'linkDialog': LinkDialog,
   6833         'linkPopover': LinkPopover,
   6834         'imageDialog': ImageDialog,
   6835         'imagePopover': ImagePopover,
   6836         'videoDialog': VideoDialog,
   6837         'helpDialog': HelpDialog,
   6838         'airPopover': AirPopover
   6839       },
   6840 
   6841       buttons: {},
   6842       
   6843       lang: 'en-US',
   6844 
   6845       // toolbar
   6846       toolbar: [
   6847         ['style', ['style']],
   6848         ['font', ['bold', 'underline', 'clear']],
   6849         ['fontname', ['fontname']],
   6850         ['color', ['color']],
   6851         ['para', ['ul', 'ol', 'paragraph']],
   6852         ['table', ['table']],
   6853         ['insert', ['link', 'picture', 'video']],
   6854         ['view', ['fullscreen', 'codeview', 'help']]
   6855       ],
   6856 
   6857       // popover
   6858       popover: {
   6859         image: [
   6860           ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
   6861           ['float', ['floatLeft', 'floatRight', 'floatNone']],
   6862           ['remove', ['removeMedia']]
   6863         ],
   6864         link: [
   6865           ['link', ['linkDialogShow', 'unlink']]
   6866         ],
   6867         air: [
   6868           ['color', ['color']],
   6869           ['font', ['bold', 'underline', 'clear']],
   6870           ['para', ['ul', 'paragraph']],
   6871           ['table', ['table']],
   6872           ['insert', ['link', 'picture']]
   6873         ]
   6874       },
   6875 
   6876       // air mode: inline editor
   6877       airMode: false,
   6878 
   6879       width: null,
   6880       height: null,
   6881 
   6882       focus: false,
   6883       tabSize: 4,
   6884       styleWithSpan: true,
   6885       shortcuts: true,
   6886       textareaAutoSync: true,
   6887       direction: null,
   6888 
   6889       styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
   6890 
   6891       fontNames: [
   6892         'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New',
   6893         'Helvetica Neue', 'Helvetica', 'Impact', 'Lucida Grande',
   6894         'Tahoma', 'Times New Roman', 'Verdana'
   6895       ],
   6896 
   6897       fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'],
   6898 
   6899       // pallete colors(n x n)
   6900       colors: [
   6901         ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'],
   6902         ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'],
   6903         ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'],
   6904         ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'],
   6905         ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'],
   6906         ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'],
   6907         ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'],
   6908         ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031']
   6909       ],
   6910 
   6911       lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'],
   6912 
   6913       tableClassName: 'table table-bordered',
   6914 
   6915       insertTableMaxSize: {
   6916         col: 10,
   6917         row: 10
   6918       },
   6919 
   6920       dialogsInBody: false,
   6921       dialogsFade: false,
   6922 
   6923       maximumImageFileSize: null,
   6924 
   6925       callbacks: {
   6926         onInit: null,
   6927         onFocus: null,
   6928         onBlur: null,
   6929         onEnter: null,
   6930         onKeyup: null,
   6931         onKeydown: null,
   6932         onImageUpload: null,
   6933         onImageUploadError: null
   6934       },
   6935 
   6936       codemirror: {
   6937         mode: 'text/html',
   6938         htmlMode: true,
   6939         lineNumbers: true
   6940       },
   6941 
   6942       keyMap: {
   6943         pc: {
   6944           'ENTER': 'insertParagraph',
   6945           'CTRL+Z': 'undo',
   6946           'CTRL+Y': 'redo',
   6947           'TAB': 'tab',
   6948           'SHIFT+TAB': 'untab',
   6949           'CTRL+B': 'bold',
   6950           'CTRL+I': 'italic',
   6951           'CTRL+U': 'underline',
   6952           'CTRL+SHIFT+S': 'strikethrough',
   6953           'CTRL+BACKSLASH': 'removeFormat',
   6954           'CTRL+SHIFT+L': 'justifyLeft',
   6955           'CTRL+SHIFT+E': 'justifyCenter',
   6956           'CTRL+SHIFT+R': 'justifyRight',
   6957           'CTRL+SHIFT+J': 'justifyFull',
   6958           'CTRL+SHIFT+NUM7': 'insertUnorderedList',
   6959           'CTRL+SHIFT+NUM8': 'insertOrderedList',
   6960           'CTRL+LEFTBRACKET': 'outdent',
   6961           'CTRL+RIGHTBRACKET': 'indent',
   6962           'CTRL+NUM0': 'formatPara',
   6963           'CTRL+NUM1': 'formatH1',
   6964           'CTRL+NUM2': 'formatH2',
   6965           'CTRL+NUM3': 'formatH3',
   6966           'CTRL+NUM4': 'formatH4',
   6967           'CTRL+NUM5': 'formatH5',
   6968           'CTRL+NUM6': 'formatH6',
   6969           'CTRL+ENTER': 'insertHorizontalRule',
   6970           'CTRL+K': 'linkDialog.show'
   6971         },
   6972 
   6973         mac: {
   6974           'ENTER': 'insertParagraph',
   6975           'CMD+Z': 'undo',
   6976           'CMD+SHIFT+Z': 'redo',
   6977           'TAB': 'tab',
   6978           'SHIFT+TAB': 'untab',
   6979           'CMD+B': 'bold',
   6980           'CMD+I': 'italic',
   6981           'CMD+U': 'underline',
   6982           'CMD+SHIFT+S': 'strikethrough',
   6983           'CMD+BACKSLASH': 'removeFormat',
   6984           'CMD+SHIFT+L': 'justifyLeft',
   6985           'CMD+SHIFT+E': 'justifyCenter',
   6986           'CMD+SHIFT+R': 'justifyRight',
   6987           'CMD+SHIFT+J': 'justifyFull',
   6988           'CMD+SHIFT+NUM7': 'insertUnorderedList',
   6989           'CMD+SHIFT+NUM8': 'insertOrderedList',
   6990           'CMD+LEFTBRACKET': 'outdent',
   6991           'CMD+RIGHTBRACKET': 'indent',
   6992           'CMD+NUM0': 'formatPara',
   6993           'CMD+NUM1': 'formatH1',
   6994           'CMD+NUM2': 'formatH2',
   6995           'CMD+NUM3': 'formatH3',
   6996           'CMD+NUM4': 'formatH4',
   6997           'CMD+NUM5': 'formatH5',
   6998           'CMD+NUM6': 'formatH6',
   6999           'CMD+ENTER': 'insertHorizontalRule',
   7000           'CMD+K': 'linkDialog.show'
   7001         }
   7002       },
   7003       icons: {
   7004         'align': 'note-icon-align',
   7005         'alignCenter': 'note-icon-align-center',
   7006         'alignJustify': 'note-icon-align-justify',
   7007         'alignLeft': 'note-icon-align-left',
   7008         'alignRight': 'note-icon-align-right',
   7009         'indent': 'note-icon-align-indent',
   7010         'outdent': 'note-icon-align-outdent',
   7011         'arrowsAlt': 'note-icon-arrows-alt',
   7012         'bold': 'note-icon-bold',
   7013         'caret': 'note-icon-caret',
   7014         'circle': 'note-icon-circle',
   7015         'close': 'note-icon-close',
   7016         'code': 'note-icon-code',
   7017         'eraser': 'note-icon-eraser',
   7018         'font': 'note-icon-font',
   7019         'frame': 'note-icon-frame',
   7020         'italic': 'note-icon-italic',
   7021         'link': 'note-icon-link',
   7022         'unlink': 'note-icon-chain-broken',
   7023         'magic': 'note-icon-magic',
   7024         'menuCheck': 'note-icon-check',
   7025         'minus': 'note-icon-minus',
   7026         'orderedlist': 'note-icon-orderedlist',
   7027         'pencil': 'note-icon-pencil',
   7028         'picture': 'note-icon-picture',
   7029         'question': 'note-icon-question',
   7030         'redo': 'note-icon-redo',
   7031         'square': 'note-icon-square',
   7032         'strikethrough': 'note-icon-strikethrough',
   7033         'subscript': 'note-icon-subscript',
   7034         'superscript': 'note-icon-superscript',
   7035         'table': 'note-icon-table',
   7036         'textHeight': 'note-icon-text-height',
   7037         'trash': 'note-icon-trash',
   7038         'underline': 'note-icon-underline',
   7039         'undo': 'note-icon-undo',
   7040         'unorderedlist': 'note-icon-unorderedlist',
   7041         'video': 'note-icon-video'
   7042       }
   7043     }
   7044   });
   7045 
   7046 }));