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