github.com/bosssauce/ponzu@v0.11.1-0.20200102001432-9bc41b703131/system/admin/static/editor/js/materialNote.js (about)

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