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}
1373 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 }); 1931 1932 var dialog = renderer.create('<div class="modal" aria-hidden="false" tabindex="-1"/>', function ($node, options) { 1933 if (options.fade) { 1934 $node.addClass('fade'); 1935 } 1936 $node.html([ 1937 '<div class="modal-dialog">', 1938 ' <div class="modal-content">', 1939 (options.title ? 1940 ' <div class="modal-header">' + 1941 ' <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>' + 1942 ' <h4 class="modal-title">' + options.title + '</h4>' + 1943 ' </div>' : '' 1944 ), 1945 ' <div class="modal-body">' + options.body + '</div>', 1946 (options.footer ? 1947 ' <div class="modal-footer">' + options.footer + '</div>' : '' 1948 ), 1949 ' </div>', 1950 '</div>' 1951 ].join('')); 1952 }); 1953 1954 var popover = renderer.create([ 1955 '<div class="note-popover popover in">', 1956 ' <div class="arrow"/>', 1957 ' <div class="popover-content note-children-container"/>', 1958 '</div>' 1959 ].join(''), function ($node, options) { 1960 var direction = typeof options.direction !== 'undefined' ? options.direction : 'bottom'; 1961 1962 $node.addClass(direction); 1963 1964 if (options.hideArrow) { 1965 $node.find('.arrow').hide(); 1966 } 1967 }); 1968 1969 var icon = function (iconClassName, tagName) { 1970 tagName = tagName || 'i'; 1971 return '<' + tagName + ' class="' + iconClassName + '"/>'; 1972 }; 1973 1974 var ui = { 1975 editor: editor, 1976 toolbar: toolbar, 1977 editingArea: editingArea, 1978 codable: codable, 1979 editable: editable, 1980 statusbar: statusbar, 1981 airEditor: airEditor, 1982 airEditable: airEditable, 1983 buttonGroup: buttonGroup, 1984 button: button, 1985 dropdown: dropdown, 1986 dropdownCheck: dropdownCheck, 1987 palette: palette, 1988 dialog: dialog, 1989 popover: popover, 1990 icon: icon, 1991 1992 toggleBtn: function ($btn, isEnable) { 1993 $btn.toggleClass('disabled', !isEnable); 1994 $btn.attr('disabled', !isEnable); 1995 }, 1996 1997 toggleBtnActive: function ($btn, isActive) { 1998 $btn.toggleClass('active', isActive); 1999 }, 2000 2001 onDialogShown: function ($dialog, handler) { 2002 $dialog.one('shown.bs.modal', handler); 2003 }, 2004 2005 onDialogHidden: function ($dialog, handler) { 2006 $dialog.one('hidden.bs.modal', handler); 2007 }, 2008 2009 showDialog: function ($dialog) { 2010 $dialog.modal('show'); 2011 }, 2012 2013 hideDialog: function ($dialog) { 2014 $dialog.modal('hide'); 2015 }, 2016 2017 createLayout: function ($note, options) { 2018 var $editor = (options.airMode ? ui.airEditor([ 2019 ui.editingArea([ 2020 ui.airEditable() 2021 ]) 2022 ]) : ui.editor([ 2023 ui.toolbar(), 2024 ui.editingArea([ 2025 ui.codable(), 2026 ui.editable() 2027 ]), 2028 ui.statusbar() 2029 ])).render(); 2030 2031 $editor.insertAfter($note); 2032 2033 return { 2034 note: $note, 2035 editor: $editor, 2036 toolbar: $editor.find('.note-toolbar'), 2037 editingArea: $editor.find('.note-editing-area'), 2038 editable: $editor.find('.note-editable'), 2039 codable: $editor.find('.note-codable'), 2040 statusbar: $editor.find('.note-statusbar') 2041 }; 2042 }, 2043 2044 removeLayout: function ($note, layoutInfo) { 2045 $note.html(layoutInfo.editable.html()); 2046 layoutInfo.editor.remove(); 2047 $note.show(); 2048 } 2049 }; 2050 2051 $.summernote = $.summernote || { 2052 lang: {} 2053 }; 2054 2055 $.extend($.summernote.lang, { 2056 'en-US': { 2057 font: { 2058 bold: 'Bold', 2059 italic: 'Italic', 2060 underline: 'Underline', 2061 clear: 'Remove Font Style', 2062 height: 'Line Height', 2063 name: 'Font Family',
2064 strikethrough: 'Strikethrough', 2065 subscript: 'Subscript', 2066 superscript: 'Superscript', 2067 size: 'Font Size' 2068 }, 2069 image: { 2070 image: 'Picture', 2071 insert: 'Insert Image', 2072 resizeFull: 'Resize Full', 2073 resizeHalf: 'Resize Half', 2074 resizeQuarter: 'Resize Quarter', 2075 floatLeft: 'Float Left', 2076 floatRight: 'Float Right', 2077 floatNone: 'Float None', 2078 shapeRounded: 'Shape: Rounded', 2079 shapeCircle: 'Shape: Circle', 2080 shapeThumbnail: 'Shape: Thumbnail', 2081 shapeNone: 'Shape: None', 2082 dragImageHere: 'Drag image or text here', 2083 dropImage: 'Drop image or Text', 2084 selectFromFiles: 'Select from files', 2085 maximumFileSize: 'Maximum file size', 2086 maximumFileSizeError: 'Maximum file size exceeded.', 2087 url: 'Image URL', 2088 remove: 'Remove Image' 2089 }, 2090 video: { 2091 video: 'Video', 2092 videoLink: 'Video Link', 2093 insert: 'Insert Video', 2094 url: 'Video URL?', 2095 providers: '(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)' 2096 }, 2097 link: { 2098 link: 'Link', 2099 insert: 'Insert Link', 2100 unlink: 'Unlink', 2101 edit: 'Edit', 2102 textToDisplay: 'Text to display', 2103 url: 'To what URL should this link go?', 2104 openInNewWindow: 'Open in new window' 2105 }, 2106 table: { 2107 table: 'Table' 2108 }, 2109 hr: { 2110 insert: 'Insert Horizontal Rule' 2111 }, 2112 style: { 2113 style: 'Style', 2114 normal: 'Normal', 2115 blockquote: 'Quote', 2116 pre: 'Code', 2117 h1: 'Header 1', 2118 h2: 'Header 2', 2119 h3: 'Header 3', 2120 h4: 'Header 4', 2121 h5: 'Header 5', 2122 h6: 'Header 6' 2123 }, 2124 lists: { 2125 unordered: 'Unordered list', 2126 ordered: 'Ordered list' 2127 }, 2128 options: { 2129 help: 'Help', 2130 fullscreen: 'Full Screen', 2131 codeview: 'Code View' 2132 }, 2133 paragraph: { 2134 paragraph: 'Paragraph', 2135 outdent: 'Outdent', 2136 indent: 'Indent', 2137 left: 'Align left', 2138 center: 'Align center', 2139 right: 'Align right', 2140 justify: 'Justify full' 2141 }, 2142 color: { 2143 recent: 'Recent Color', 2144 more: 'More Color', 2145 background: 'Background Color', 2146 foreground: 'Foreground Color', 2147 transparent: 'Transparent', 2148 setTransparent: 'Set transparent', 2149 reset: 'Reset', 2150 resetToDefault: 'Reset to default' 2151 }, 2152 shortcut: { 2153 shortcuts: 'Keyboard shortcuts', 2154 close: 'Close', 2155 textFormatting: 'Text formatting', 2156 action: 'Action', 2157 paragraphFormatting: 'Paragraph formatting', 2158 documentStyle: 'Document Style', 2159 extraKeys: 'Extra keys' 2160 }, 2161 help: { 2162 'insertParagraph': 'Insert Paragraph', 2163 'undo': 'Undoes the last command', 2164 'redo': 'Redoes the last command', 2165 'tab': 'Tab', 2166 'untab': 'Untab', 2167 'bold': 'Set a bold style', 2168 'italic': 'Set a italic style', 2169 'underline': 'Set a underline style', 2170 'strikethrough': 'Set a strikethrough style', 2171 'removeFormat': 'Clean a style', 2172 'justifyLeft': 'Set left align', 2173 'justifyCenter': 'Set center align', 2174 'justifyRight': 'Set right align', 2175 'justifyFull': 'Set full align', 2176 'insertUnorderedList': 'Toggle unordered list', 2177 'insertOrderedList': 'Toggle ordered list', 2178 'outdent': 'Outdent on current paragraph', 2179 'indent': 'Indent on current paragraph', 2180 'formatPara': 'Change current block\'s format as a paragraph(P tag)', 2181 'formatH1': 'Change current block\'s format as H1', 2182 'formatH2': 'Change current block\'s format as H2', 2183 'formatH3': 'Change current block\'s format as H3', 2184 'formatH4': 'Change current block\'s format as H4', 2185 'formatH5': 'Change current block\'s format as H5', 2186 'formatH6': 'Change current block\'s format as H6', 2187 'insertHorizontalRule': 'Insert horizontal rule', 2188 'linkDialog.show': 'Show Link Dialog' 2189 }, 2190 history: { 2191 undo: 'Undo', 2192 redo: 'Redo' 2193 }, 2194 specialChar: { 2195 specialChar: 'SPECIAL CHARACTERS', 2196 select: 'Select Special characters' 2197 } 2198 } 2199 }); 2200 2201 2202 /** 2203 * @class core.key 2204 * 2205 * Object for keycodes. 2206 * 2207 * @singleton 2208 * @alternateClassName key 2209 */ 2210 var key = (function () { 2211 var keyMap = { 2212 'BACKSPACE': 8, 2213 'TAB': 9, 2214 'ENTER': 13, 2215 'SPACE': 32, 2216 2217 // Arrow 2218 'LEFT': 37, 2219 'UP': 38, 2220 'RIGHT': 39, 2221 'DOWN': 40, 2222 2223 // Number: 0-9 2224 'NUM0': 48, 2225 'NUM1': 49, 2226 'NUM2': 50, 2227 'NUM3': 51, 2228 'NUM4': 52, 2229 'NUM5': 53, 2230 'NUM6': 54, 2231 'NUM7': 55, 2232 'NUM8': 56, 2233 2234 // Alphabet: a-z 2235 'B': 66, 2236 'E': 69, 2237 'I': 73, 2238 'J': 74, 2239 'K': 75, 2240 'L': 76, 2241 'R': 82, 2242 'S': 83, 2243 'U': 85, 2244 'V': 86, 2245 'Y': 89, 2246 'Z': 90, 2247 2248 'SLASH': 191, 2249 'LEFTBRACKET': 219, 2250 'BACKSLASH': 220, 2251 'RIGHTBRACKET': 221 2252 }; 2253 2254 return { 2255 /** 2256 * @method isEdit 2257 * 2258 * @param {Number} keyCode 2259 * @return {Boolean} 2260 */ 2261 isEdit: function (keyCode) { 2262 return list.contains([ 2263 keyMap.BACKSPACE, 2264 keyMap.TAB, 2265 keyMap.ENTER, 2266 keyMap.SPACE 2267 ], keyCode); 2268 }, 2269 /** 2270 * @method isMove 2271 * 2272 * @param {Number} keyCode 2273 * @return {Boolean} 2274 */ 2275 isMove: function (keyCode) { 2276 return list.contains([ 2277 keyMap.LEFT, 2278 keyMap.UP, 2279 keyMap.RIGHT, 2280 keyMap.DOWN 2281 ], keyCode); 2282 }, 2283 /** 2284 * @property {Object} nameFromCode 2285 * @property {String} nameFromCode.8 "BACKSPACE" 2286 */ 2287 nameFromCode: func.invertObject(keyMap), 2288 code: keyMap 2289 }; 2290 })(); 2291 2292 var range = (function () { 2293 2294 /** 2295 * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js 2296 * 2297 * @param {TextRange} textRange 2298 * @param {Boolean} isStart 2299 * @return {BoundaryPoint} 2300 *
2301 * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx 2302 */ 2303 var textRangeToPoint = function (textRange, isStart) { 2304 var container = textRange.parentElement(), offset; 2305 2306 var tester = document.body.createTextRange(), prevContainer; 2307 var childNodes = list.from(container.childNodes); 2308 for (offset = 0; offset < childNodes.length; offset++) { 2309 if (dom.isText(childNodes[offset])) { 2310 continue; 2311 } 2312 tester.moveToElementText(childNodes[offset]); 2313 if (tester.compareEndPoints('StartToStart', textRange) >= 0) { 2314 break; 2315 } 2316 prevContainer = childNodes[offset]; 2317 } 2318 2319 if (offset !== 0 && dom.isText(childNodes[offset - 1])) { 2320 var textRangeStart = document.body.createTextRange(), curTextNode = null; 2321 textRangeStart.moveToElementText(prevContainer || container); 2322 textRangeStart.collapse(!prevContainer); 2323 curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild; 2324 2325 var pointTester = textRange.duplicate(); 2326 pointTester.setEndPoint('StartToStart', textRangeStart); 2327 var textCount = pointTester.text.replace(/[\r\n]/g, '').length; 2328 2329 while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) { 2330 textCount -= curTextNode.nodeValue.length; 2331 curTextNode = curTextNode.nextSibling; 2332 } 2333 2334 /* jshint ignore:start */ 2335 var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack 2336 /* jshint ignore:end */ 2337 2338 if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) && 2339 textCount === curTextNode.nodeValue.length) { 2340 textCount -= curTextNode.nodeValue.length; 2341 curTextNode = curTextNode.nextSibling; 2342 } 2343 2344 container = curTextNode; 2345 offset = textCount; 2346 } 2347 2348 return { 2349 cont: container, 2350 offset: offset 2351 }; 2352 }; 2353 2354 /** 2355 * return TextRange from boundary point (inspired by google closure-library) 2356 * @param {BoundaryPoint} point 2357 * @return {TextRange} 2358 */ 2359 var pointToTextRange = function (point) { 2360 var textRangeInfo = function (container, offset) { 2361 var node, isCollapseToStart; 2362 2363 if (dom.isText(container)) { 2364 var prevTextNodes = dom.listPrev(container, func.not(dom.isText)); 2365 var prevContainer = list.last(prevTextNodes).previousSibling; 2366 node = prevContainer || container.parentNode; 2367 offset += list.sum(list.tail(prevTextNodes), dom.nodeLength); 2368 isCollapseToStart = !prevContainer; 2369 } else { 2370 node = container.childNodes[offset] || container; 2371 if (dom.isText(node)) { 2372 return textRangeInfo(node, 0); 2373 } 2374 2375 offset = 0; 2376 isCollapseToStart = false;
2377 } 2378 2379 return { 2380 node: node, 2381 collapseToStart: isCollapseToStart, 2382 offset: offset 2383 }; 2384 }; 2385 2386 var textRange = document.body.createTextRange(); 2387 var info = textRangeInfo(point.node, point.offset); 2388 2389 textRange.moveToElementText(info.node); 2390 textRange.collapse(info.collapseToStart); 2391 textRange.moveStart('character', info.offset); 2392 return textRange; 2393 }; 2394 2395 /** 2396 * Wrapped Range 2397 * 2398 * @constructor 2399 * @param {Node} sc - start container 2400 * @param {Number} so - start offset 2401 * @param {Node} ec - end container 2402 * @param {Number} eo - end offset 2403 */ 2404 var WrappedRange = function (sc, so, ec, eo) { 2405 this.sc = sc; 2406 this.so = so; 2407 this.ec = ec; 2408 this.eo = eo; 2409 2410 // nativeRange: get nativeRange from sc, so, ec, eo 2411 var nativeRange = function () { 2412 if (agent.isW3CRangeSupport) { 2413 var w3cRange = document.createRange(); 2414 w3cRange.setStart(sc, so); 2415 w3cRange.setEnd(ec, eo); 2416 2417 return w3cRange; 2418 } else { 2419 var textRange = pointToTextRange({ 2420 node: sc, 2421 offset: so 2422 }); 2423 2424 textRange.setEndPoint('EndToEnd', pointToTextRange({ 2425 node: ec, 2426 offset: eo 2427 })); 2428 2429 return textRange; 2430 } 2431 }; 2432 2433 this.getPoints = function () { 2434 return { 2435 sc: sc, 2436 so: so, 2437 ec: ec, 2438 eo: eo 2439 }; 2440 }; 2441
2442 this.getStartPoint = function () { 2443 return { 2444 node: sc, 2445 offset: so 2446 }; 2447 }; 2448 2449 this.getEndPoint = function () { 2450 return { 2451 node: ec, 2452 offset: eo 2453 }; 2454 }; 2455 2456 /** 2457 * select update visible range 2458 */ 2459 this.select = function () { 2460 var nativeRng = nativeRange(); 2461 if (agent.isW3CRangeSupport) { 2462 var selection = document.getSelection(); 2463 if (selection.rangeCount > 0) { 2464 selection.removeAllRanges(); 2465 } 2466 selection.addRange(nativeRng); 2467 } else { 2468 nativeRng.select(); 2469 } 2470 2471 return this; 2472 }; 2473 2474 /** 2475 * Moves the scrollbar to start container(sc) of current range 2476 * 2477 * @return {WrappedRange} 2478 */ 2479 this.scrollIntoView = function (container) { 2480 var height = $(container).height(); 2481 if (container.scrollTop + height < this.sc.offsetTop) { 2482 container.scrollTop += Math.abs(container.scrollTop + height - this.sc.offsetTop); 2483 } 2484 2485 return this; 2486 }; 2487 2488 /** 2489 * @return {WrappedRange} 2490 */ 2491 this.normalize = function () { 2492 2493 /** 2494 * @param {BoundaryPoint} point 2495 * @param {Boolean} isLeftToRight 2496 * @return {BoundaryPoint} 2497 */ 2498 var getVisiblePoint = function (point, isLeftToRight) { 2499 if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) || 2500 (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) || 2501 (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) || 2502 (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) { 2503 return point; 2504 } 2505 2506 // point on block's edge 2507 var block = dom.ancestor(point.node, dom.isBlock); 2508 if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) || 2509 ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) { 2510 2511 // returns point already on visible point 2512 if (dom.isVisiblePoint(point)) { 2513 return point; 2514 } 2515 // reverse direction 2516 isLeftToRight = !isLeftToRight; 2517 } 2518 2519 var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) : 2520 dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint); 2521 return nextPoint || point; 2522 }; 2523 2524 var endPoint = getVisiblePoint(this.getEndPoint(), false); 2525 var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true); 2526 2527 return new WrappedRange( 2528 startPoint.node, 2529 startPoint.offset, 2530 endPoint.node, 2531 endPoint.offset 2532 ); 2533 }; 2534 2535 /** 2536 * returns matched nodes on range 2537 * 2538 * @param {Function} [pred] - predicate function 2539 * @param {Object} [options] 2540 * @param {Boolean} [options.includeAncestor] 2541 * @param {Boolean} [options.fullyContains] 2542 * @return {Node[]} 2543 */ 2544 this.nodes = function (pred, options) { 2545 pred = pred || func.ok; 2546 2547 var includeAncestor = options && options.includeAncestor; 2548 var fullyContains = options && options.fullyContains; 2549 2550 // TODO compare points and sort 2551 var startPoint = this.getStartPoint(); 2552 var endPoint = this.getEndPoint(); 2553 2554 var nodes = []; 2555 var leftEdgeNodes = []; 2556 2557 dom.walkPoint(startPoint, endPoint, function (point) { 2558 if (dom.isEditable(point.node)) { 2559 return; 2560 } 2561 2562 var node; 2563 if (fullyContains) { 2564 if (dom.isLeftEdgePoint(point)) { 2565 leftEdgeNodes.push(point.node); 2566 } 2567 if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) { 2568 node = point.node;
2569 } 2570 } else if (includeAncestor) { 2571 node = dom.ancestor(point.node, pred); 2572 } else { 2573 node = point.node; 2574 } 2575 2576 if (node && pred(node)) { 2577 nodes.push(node); 2578 } 2579 }, true); 2580 2581 return list.unique(nodes); 2582 }; 2583 2584 /** 2585 * returns commonAncestor of range 2586 * @return {Element} - commonAncestor 2587 */ 2588 this.commonAncestor = function () { 2589 return dom.commonAncestor(sc, ec); 2590 }; 2591 2592 /** 2593 * returns expanded range by pred 2594 * 2595 * @param {Function} pred - predicate function 2596 * @return {WrappedRange} 2597 */ 2598 this.expand = function (pred) { 2599 var startAncestor = dom.ancestor(sc, pred); 2600 var endAncestor = dom.ancestor(ec, pred); 2601 2602 if (!startAncestor && !endAncestor) { 2603 return new WrappedRange(sc, so, ec, eo); 2604 } 2605 2606 var boundaryPoints = this.getPoints(); 2607 2608 if (startAncestor) { 2609 boundaryPoints.sc = startAncestor; 2610 boundaryPoints.so = 0; 2611 } 2612 2613 if (endAncestor) { 2614 boundaryPoints.ec = endAncestor; 2615 boundaryPoints.eo = dom.nodeLength(endAncestor); 2616 } 2617 2618 return new WrappedRange( 2619 boundaryPoints.sc, 2620 boundaryPoints.so, 2621 boundaryPoints.ec, 2622 boundaryPoints.eo 2623 ); 2624 }; 2625 2626 /** 2627 * @param {Boolean} isCollapseToStart 2628 * @return {WrappedRange} 2629 */ 2630 this.collapse = function (isCollapseToStart) { 2631 if (isCollapseToStart) { 2632 return new WrappedRange(sc, so, sc, so); 2633 } else { 2634 return new WrappedRange(ec, eo, ec, eo); 2635 } 2636 }; 2637 2638 /** 2639 * splitText on range 2640 */ 2641 this.splitText = function () { 2642 var isSameContainer = sc === ec; 2643 var boundaryPoints = this.getPoints(); 2644 2645 if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) { 2646 ec.splitText(eo); 2647 } 2648 2649 if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) { 2650 boundaryPoints.sc = sc.splitText(so); 2651 boundaryPoints.so = 0; 2652 2653 if (isSameContainer) { 2654 boundaryPoints.ec = boundaryPoints.sc; 2655 boundaryPoints.eo = eo - so; 2656 } 2657 } 2658 2659 return new WrappedRange( 2660 boundaryPoints.sc, 2661 boundaryPoints.so, 2662 boundaryPoints.ec, 2663 boundaryPoints.eo 2664 ); 2665 }; 2666 2667 /** 2668 * delete contents on range 2669 * @return {WrappedRange} 2670 */ 2671 this.deleteContents = function () { 2672 if (this.isCollapsed()) { 2673 return this; 2674 } 2675 2676 var rng = this.splitText(); 2677 var nodes = rng.nodes(null, { 2678 fullyContains: true 2679 }); 2680 2681 // find new cursor point 2682 var point = dom.prevPointUntil(rng.getStartPoint(), function (point) { 2683 return !list.contains(nodes, point.node); 2684 }); 2685 2686 var emptyParents = []; 2687 $.each(nodes, function (idx, node) { 2688 // find empty parents 2689 var parent = node.parentNode; 2690 if (point.node !== parent && dom.nodeLength(parent) === 1) { 2691 emptyParents.push(parent); 2692 } 2693 dom.remove(node, false); 2694 }); 2695 2696 // remove empty parents 2697 $.each(emptyParents, function (idx, node) { 2698 dom.remove(node, false); 2699 }); 2700 2701 return new WrappedRange( 2702 point.node, 2703 point.offset, 2704 point.node, 2705 point.offset 2706 ).normalize(); 2707 }; 2708 2709 /** 2710 * makeIsOn: return isOn(pred) function 2711 */ 2712 var makeIsOn = function (pred) { 2713 return function () { 2714 var ancestor = dom.ancestor(sc, pred); 2715 return !!ancestor && (ancestor === dom.ancestor(ec, pred)); 2716 }; 2717 }; 2718 2719 // isOnEditable: judge whether range is on editable or not 2720 this.isOnEditable = makeIsOn(dom.isEditable); 2721 // isOnList: judge whether range is on list node or not 2722 this.isOnList = makeIsOn(dom.isList); 2723 // isOnAnchor: judge whether range is on anchor node or not 2724 this.isOnAnchor = makeIsOn(dom.isAnchor); 2725 // isOnCell: judge whether range is on cell node or not 2726 this.isOnCell = makeIsOn(dom.isCell); 2727 // isOnData: judge whether range is on data node or not 2728 this.isOnData = makeIsOn(dom.isData); 2729 2730 /** 2731 * @param {Function} pred 2732 * @return {Boolean} 2733 */ 2734 this.isLeftEdgeOf = function (pred) { 2735 if (!dom.isLeftEdgePoint(this.getStartPoint())) { 2736 return false;
2737 } 2738 2739 var node = dom.ancestor(this.sc, pred); 2740 return node && dom.isLeftEdgeOf(this.sc, node); 2741 }; 2742 2743 /** 2744 * returns whether range was collapsed or not 2745 */ 2746 this.isCollapsed = function () { 2747 return sc === ec && so === eo; 2748 }; 2749 2750 /** 2751 * wrap inline nodes which children of body with paragraph 2752 * 2753 * @return {WrappedRange} 2754 */ 2755 this.wrapBodyInlineWithPara = function () { 2756 if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) { 2757 sc.innerHTML = dom.emptyPara; 2758 return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0); 2759 } 2760 2761 /** 2762 * [workaround] firefox often create range on not visible point. so normalize here. 2763 * - firefox: |<p>text</p>| 2764 * - chrome: <p>|text|</p> 2765 */ 2766 var rng = this.normalize(); 2767 if (dom.isParaInline(sc) || dom.isPara(sc)) { 2768 return rng; 2769 } 2770 2771 // find inline top ancestor 2772 var topAncestor; 2773 if (dom.isInline(rng.sc)) { 2774 var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline)); 2775 topAncestor = list.last(ancestors); 2776 if (!dom.isInline(topAncestor)) { 2777 topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so]; 2778 } 2779 } else { 2780 topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0]; 2781 } 2782 2783 // siblings not in paragraph 2784 var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse(); 2785 inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline)); 2786 2787 // wrap with paragraph 2788 if (inlineSiblings.length) { 2789 var para = dom.wrap(list.head(inlineSiblings), 'p'); 2790 dom.appendChildNodes(para, list.tail(inlineSiblings)); 2791 } 2792 2793 return this.normalize(); 2794 }; 2795 2796 /** 2797 * insert node at current cursor 2798 * 2799 * @param {Node} node 2800 * @return {Node} 2801 */ 2802 this.insertNode = function (node) { 2803 var rng = this.wrapBodyInlineWithPara().deleteContents(); 2804 var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node)); 2805 2806 if (info.rightNode) { 2807 info.rightNode.parentNode.insertBefore(node, info.rightNode); 2808 } else { 2809 info.container.appendChild(node); 2810 } 2811 2812 return node; 2813 }; 2814 2815 /** 2816 * insert html at current cursor 2817 */ 2818 this.pasteHTML = function (markup) {
2819 var contentsContainer = $('<div></div>').html(markup)[0]; 2820 var childNodes = list.from(contentsContainer.childNodes); 2821 2822 var rng = this.wrapBodyInlineWithPara().deleteContents(); 2823 2824 return childNodes.reverse().map(function (childNode) { 2825 return rng.insertNode(childNode); 2826 }).reverse(); 2827 }; 2828 2829 /** 2830 * returns text in range 2831 * 2832 * @return {String} 2833 */ 2834 this.toString = function () { 2835 var nativeRng = nativeRange(); 2836 return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text; 2837 }; 2838 2839 /** 2840 * returns range for word before cursor 2841 * 2842 * @param {Boolean} [findAfter] - find after cursor, default: false 2843 * @return {WrappedRange} 2844 */ 2845 this.getWordRange = function (findAfter) { 2846 var endPoint = this.getEndPoint(); 2847 2848 if (!dom.isCharPoint(endPoint)) { 2849 return this; 2850 } 2851 2852 var startPoint = dom.prevPointUntil(endPoint, function (point) { 2853 return !dom.isCharPoint(point); 2854 }); 2855 2856 if (findAfter) { 2857 endPoint = dom.nextPointUntil(endPoint, function (point) { 2858 return !dom.isCharPoint(point); 2859 }); 2860 } 2861 2862 return new WrappedRange( 2863 startPoint.node, 2864 startPoint.offset, 2865 endPoint.node, 2866 endPoint.offset 2867 ); 2868 }; 2869 2870 /** 2871 * create offsetPath bookmark 2872 * 2873 * @param {Node} editable 2874 */ 2875 this.bookmark = function (editable) { 2876 return { 2877 s: { 2878 path: dom.makeOffsetPath(editable, sc), 2879 offset: so 2880 }, 2881 e: { 2882 path: dom.makeOffsetPath(editable, ec), 2883 offset: eo 2884 } 2885 }; 2886 }; 2887 2888 /** 2889 * create offsetPath bookmark base on paragraph 2890 * 2891 * @param {Node[]} paras 2892 */ 2893 this.paraBookmark = function (paras) { 2894 return { 2895 s: { 2896 path: list.tail(dom.makeOffsetPath(list.head(paras), sc)), 2897 offset: so 2898 }, 2899 e: { 2900 path: list.tail(dom.makeOffsetPath(list.last(paras), ec)), 2901 offset: eo 2902 } 2903 }; 2904 }; 2905 2906 /** 2907 * getClientRects 2908 * @return {Rect[]} 2909 */ 2910 this.getClientRects = function () { 2911 var nativeRng = nativeRange(); 2912 return nativeRng.getClientRects(); 2913 }; 2914 }; 2915 2916 /** 2917 * @class core.range 2918 * 2919 * Data structure 2920 * * BoundaryPoint: a point of dom tree 2921 * * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range 2922 * 2923 * See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position 2924 * 2925 * @singleton 2926 * @alternateClassName range 2927 */ 2928 return { 2929 /** 2930 * create Range Object From arguments or Browser Selection 2931 * 2932 * @param {Node} sc - start container 2933 * @param {Number} so - start offset 2934 * @param {Node} ec - end container 2935 * @param {Number} eo - end offset 2936 * @return {WrappedRange} 2937 */ 2938 create: function (sc, so, ec, eo) { 2939 if (arguments.length === 4) { 2940 return new WrappedRange(sc, so, ec, eo); 2941 } else if (arguments.length === 2) { //collapsed 2942 ec = sc; 2943 eo = so; 2944 return new WrappedRange(sc, so, ec, eo); 2945 } else { 2946 var wrappedRange = this.createFromSelection(); 2947 if (!wrappedRange && arguments.length === 1) { 2948 wrappedRange = this.createFromNode(arguments[0]); 2949 return wrappedRange.collapse(dom.emptyPara === arguments[0].innerHTML); 2950 } 2951 return wrappedRange; 2952 } 2953 }, 2954 2955 createFromSelection: function () { 2956 var sc, so, ec, eo; 2957 if (agent.isW3CRangeSupport) { 2958 var selection = document.getSelection(); 2959 if (!selection || selection.rangeCount === 0) { 2960 return null; 2961 } else if (dom.isBody(selection.anchorNode)) { 2962 // Firefox: returns entire body as range on initialization. 2963 // We won't never need it. 2964 return null; 2965 } 2966 2967 var nativeRng = selection.getRangeAt(0); 2968 sc = nativeRng.startContainer; 2969 so = nativeRng.startOffset; 2970 ec = nativeRng.endContainer; 2971 eo = nativeRng.endOffset; 2972 } else { // IE8: TextRange 2973 var textRange = document.selection.createRange(); 2974 var textRangeEnd = textRange.duplicate(); 2975 textRangeEnd.collapse(false); 2976 var textRangeStart = textRange; 2977 textRangeStart.collapse(true); 2978 2979 var startPoint = textRangeToPoint(textRangeStart, true), 2980 endPoint = textRangeToPoint(textRangeEnd, false); 2981 2982 // same visible point case: range was collapsed. 2983 if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) && 2984 dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) && 2985 endPoint.node.nextSibling === startPoint.node) { 2986 startPoint = endPoint; 2987 } 2988 2989 sc = startPoint.cont; 2990 so = startPoint.offset; 2991 ec = endPoint.cont; 2992 eo = endPoint.offset; 2993 } 2994 2995 return new WrappedRange(sc, so, ec, eo); 2996 }, 2997 2998 /** 2999 * @method 3000 * 3001 * create WrappedRange from node 3002 * 3003 * @param {Node} node 3004 * @return {WrappedRange} 3005 */ 3006 createFromNode: function (node) { 3007 var sc = node; 3008 var so = 0; 3009 var ec = node; 3010 var eo = dom.nodeLength(ec); 3011 3012 // browsers can't target a picture or void node 3013 if (dom.isVoid(sc)) { 3014 so = dom.listPrev(sc).length - 1; 3015 sc = sc.parentNode; 3016 } 3017 if (dom.isBR(ec)) { 3018 eo = dom.listPrev(ec).length - 1; 3019 ec = ec.parentNode; 3020 } else if (dom.isVoid(ec)) { 3021 eo = dom.listPrev(ec).length; 3022 ec = ec.parentNode; 3023 } 3024 3025 return this.create(sc, so, ec, eo); 3026 }, 3027 3028 /** 3029 * create WrappedRange from node after position 3030 * 3031 * @param {Node} node 3032 * @return {WrappedRange} 3033 */ 3034 createFromNodeBefore: function (node) { 3035 return this.createFromNode(node).collapse(true); 3036 }, 3037 3038 /** 3039 * create WrappedRange from node after position 3040 * 3041 * @param {Node} node 3042 * @return {WrappedRange} 3043 */ 3044 createFromNodeAfter: function (node) { 3045 return this.createFromNode(node).collapse(); 3046 }, 3047 3048 /** 3049 * @method 3050 * 3051 * create WrappedRange from bookmark 3052 * 3053 * @param {Node} editable 3054 * @param {Object} bookmark 3055 * @return {WrappedRange} 3056 */ 3057 createFromBookmark: function (editable, bookmark) { 3058 var sc = dom.fromOffsetPath(editable, bookmark.s.path); 3059 var so = bookmark.s.offset; 3060 var ec = dom.fromOffsetPath(editable, bookmark.e.path); 3061 var eo = bookmark.e.offset; 3062 return new WrappedRange(sc, so, ec, eo); 3063 }, 3064 3065 /** 3066 * @method 3067 * 3068 * create WrappedRange from paraBookmark 3069 * 3070 * @param {Object} bookmark 3071 * @param {Node[]} paras 3072 * @return {WrappedRange} 3073 */ 3074 createFromParaBookmark: function (bookmark, paras) { 3075 var so = bookmark.s.offset; 3076 var eo = bookmark.e.offset; 3077 var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path); 3078 var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path); 3079 3080 return new WrappedRange(sc, so, ec, eo); 3081 } 3082 }; 3083 })(); 3084 3085 /** 3086 * @class core.async 3087 * 3088 * Async functions which returns `Promise` 3089 * 3090 * @singleton 3091 * @alternateClassName async 3092 */ 3093 var async = (function () { 3094 /** 3095 * @method readFileAsDataURL 3096 * 3097 * read contents of file as representing URL 3098 * 3099 * @param {File} file 3100 * @return {Promise} - then: dataUrl 3101 */ 3102 var readFileAsDataURL = function (file) { 3103 return $.Deferred(function (deferred) { 3104 $.extend(new FileReader(), { 3105 onload: function (e) { 3106 var dataURL = e.target.result; 3107 deferred.resolve(dataURL); 3108 }, 3109 onerror: function () { 3110 deferred.reject(this); 3111 } 3112 }).readAsDataURL(file); 3113 }).promise(); 3114 }; 3115 3116 /** 3117 * @method createImage 3118 * 3119 * create `<image>` from url string 3120 * 3121 * @param {String} url 3122 * @return {Promise} - then: $image 3123 */ 3124 var createImage = function (url) { 3125 return $.Deferred(function (deferred) { 3126 var $img = $('<img>'); 3127 3128 $img.one('load', function () { 3129 $img.off('error abort'); 3130 deferred.resolve($img); 3131 }).one('error abort', function () { 3132 $img.off('load').detach(); 3133 deferred.reject($img); 3134 }).css({ 3135 display: 'none' 3136 }).appendTo(document.body).attr('src', url); 3137 }).promise(); 3138 }; 3139 3140 return { 3141 readFileAsDataURL: readFileAsDataURL, 3142 createImage: createImage 3143 }; 3144 })(); 3145 3146 /** 3147 * @class editing.History 3148 * 3149 * Editor History 3150 * 3151 */ 3152 var History = function ($editable) { 3153 var stack = [], stackOffset = -1; 3154 var editable = $editable[0]; 3155 3156 var makeSnapshot = function () { 3157 var rng = range.create(editable); 3158 var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}}; 3159 3160 return { 3161 contents: $editable.html(), 3162 bookmark: (rng ? rng.bookmark(editable) : emptyBookmark) 3163 }; 3164 }; 3165
3166 var applySnapshot = function (snapshot) { 3167 if (snapshot.contents !== null) { 3168 $editable.html(snapshot.contents); 3169 } 3170 if (snapshot.bookmark !== null) { 3171 range.createFromBookmark(editable, snapshot.bookmark).select(); 3172 } 3173 }; 3174 3175 /** 3176 * @method rewind 3177 * Rewinds the history stack back to the first snapshot taken. 3178 * Leaves the stack intact, so that "Redo" can still be used. 3179 */ 3180 this.rewind = function () { 3181 // Create snap shot if not yet recorded 3182 if ($editable.html() !== stack[stackOffset].contents) { 3183 this.recordUndo(); 3184 } 3185 3186 // Return to the first available snapshot. 3187 stackOffset = 0; 3188 3189 // Apply that snapshot. 3190 applySnapshot(stack[stackOffset]); 3191 }; 3192 3193 /** 3194 * @method reset 3195 * Resets the history stack completely; reverting to an empty editor. 3196 */ 3197 this.reset = function () { 3198 // Clear the stack. 3199 stack = []; 3200 3201 // Restore stackOffset to its original value. 3202 stackOffset = -1; 3203 3204 // Clear the editable area. 3205 $editable.html(''); 3206 3207 // Record our first snapshot (of nothing). 3208 this.recordUndo(); 3209 }; 3210 3211 /** 3212 * undo 3213 */ 3214 this.undo = function () { 3215 // Create snap shot if not yet recorded 3216 if ($editable.html() !== stack[stackOffset].contents) { 3217 this.recordUndo(); 3218 } 3219 3220 if (0 < stackOffset) { 3221 stackOffset--; 3222 applySnapshot(stack[stackOffset]); 3223 } 3224 }; 3225 3226 /** 3227 * redo 3228 */ 3229 this.redo = function () { 3230 if (stack.length - 1 > stackOffset) { 3231 stackOffset++; 3232 applySnapshot(stack[stackOffset]); 3233 } 3234 }; 3235 3236 /** 3237 * recorded undo 3238 */ 3239 this.recordUndo = function () { 3240 stackOffset++; 3241 3242 // Wash out stack after stackOffset 3243 if (stack.length > stackOffset) { 3244 stack = stack.slice(0, stackOffset); 3245 } 3246 3247 // Create new snapshot and push it to the end 3248 stack.push(makeSnapshot()); 3249 }; 3250 }; 3251 3252 /** 3253 * @class editing.Style 3254 * 3255 * Style 3256 * 3257 */ 3258 var Style = function () { 3259 /** 3260 * @method jQueryCSS 3261 * 3262 * [workaround] for old jQuery 3263 * passing an array of style properties to .css() 3264 * will result in an object of property-value pairs. 3265 * (compability with version < 1.9) 3266 * 3267 * @private 3268 * @param {jQuery} $obj 3269 * @param {Array} propertyNames - An array of one or more CSS properties. 3270 * @return {Object} 3271 */ 3272 var jQueryCSS = function ($obj, propertyNames) { 3273 if (agent.jqueryVersion < 1.9) { 3274 var result = {}; 3275 $.each(propertyNames, function (idx, propertyName) { 3276 result[propertyName] = $obj.css(propertyName); 3277 }); 3278 return result; 3279 } 3280 return $obj.css.call($obj, propertyNames); 3281 }; 3282 3283 /** 3284 * returns style object from node 3285 * 3286 * @param {jQuery} $node 3287 * @return {Object} 3288 */ 3289 this.fromNode = function ($node) { 3290 var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height']; 3291 var styleInfo = jQueryCSS($node, properties) || {}; 3292 styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10); 3293 return styleInfo; 3294 }; 3295 3296 /** 3297 * paragraph level style 3298 * 3299 * @param {WrappedRange} rng 3300 * @param {Object} styleInfo 3301 */ 3302 this.stylePara = function (rng, styleInfo) { 3303 $.each(rng.nodes(dom.isPara, { 3304 includeAncestor: true 3305 }), function (idx, para) { 3306 $(para).css(styleInfo); 3307 }); 3308 }; 3309 3310 /** 3311 * insert and returns styleNodes on range. 3312 * 3313 * @param {WrappedRange} rng 3314 * @param {Object} [options] - options for styleNodes 3315 * @param {String} [options.nodeName] - default: `SPAN` 3316 * @param {Boolean} [options.expandClosestSibling] - default: `false` 3317 * @param {Boolean} [options.onlyPartialContains] - default: `false` 3318 * @return {Node[]} 3319 */ 3320 this.styleNodes = function (rng, options) { 3321 rng = rng.splitText(); 3322 3323 var nodeName = options && options.nodeName || 'SPAN'; 3324 var expandClosestSibling = !!(options && options.expandClosestSibling); 3325 var onlyPartialContains = !!(options && options.onlyPartialContains); 3326 3327 if (rng.isCollapsed()) {
3328 return [rng.insertNode(dom.create(nodeName))]; 3329 } 3330 3331 var pred = dom.makePredByNodeName(nodeName); 3332 var nodes = rng.nodes(dom.isText, { 3333 fullyContains: true 3334 }).map(function (text) { 3335 return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName); 3336 }); 3337 3338 if (expandClosestSibling) { 3339 if (onlyPartialContains) { 3340 var nodesInRange = rng.nodes(); 3341 // compose with partial contains predication 3342 pred = func.and(pred, function (node) { 3343 return list.contains(nodesInRange, node); 3344 }); 3345 } 3346 3347 return nodes.map(function (node) { 3348 var siblings = dom.withClosestSiblings(node, pred); 3349 var head = list.head(siblings); 3350 var tails = list.tail(siblings); 3351 $.each(tails, function (idx, elem) { 3352 dom.appendChildNodes(head, elem.childNodes); 3353 dom.remove(elem); 3354 }); 3355 return list.head(siblings); 3356 }); 3357 } else { 3358 return nodes; 3359 } 3360 }; 3361 3362 /** 3363 * get current style on cursor 3364 * 3365 * @param {WrappedRange} rng 3366 * @return {Object} - object contains style properties. 3367 */ 3368 this.current = function (rng) { 3369 var $cont = $(!dom.isElement(rng.sc) ? rng.sc.parentNode : rng.sc); 3370 var styleInfo = this.fromNode($cont); 3371 3372 // document.queryCommandState for toggle state 3373 // [workaround] prevent Firefox nsresult: "0x80004005 (NS_ERROR_FAILURE)" 3374 try { 3375 styleInfo = $.extend(styleInfo, { 3376 'font-bold': document.queryCommandState('bold') ? 'bold' : 'normal', 3377 'font-italic': document.queryCommandState('italic') ? 'italic' : 'normal', 3378 'font-underline': document.queryCommandState('underline') ? 'underline' : 'normal', 3379 'font-subscript': document.queryCommandState('subscript') ? 'subscript' : 'normal', 3380 'font-superscript': document.queryCommandState('superscript') ? 'superscript' : 'normal', 3381 'font-strikethrough': document.queryCommandState('strikethrough') ? 'strikethrough' : 'normal' 3382 }); 3383 } catch (e) {} 3384 3385 // list-style-type to list-style(unordered, ordered) 3386 if (!rng.isOnList()) { 3387 styleInfo['list-style'] = 'none'; 3388 } else { 3389 var orderedTypes = ['circle', 'disc', 'disc-leading-zero', 'square']; 3390 var isUnordered = $.inArray(styleInfo['list-style-type'], orderedTypes) > -1; 3391 styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered'; 3392 } 3393 3394 var para = dom.ancestor(rng.sc, dom.isPara); 3395 if (para && para.style['line-height']) { 3396 styleInfo['line-height'] = para.style.lineHeight; 3397 } else { 3398 var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10); 3399 styleInfo['line-height'] = lineHeight.toFixed(1); 3400 } 3401 3402 styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor); 3403 styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable); 3404 styleInfo.range = rng; 3405 3406 return styleInfo; 3407 }; 3408 }; 3409 3410 3411 /** 3412 * @class editing.Bullet 3413 * 3414 * @alternateClassName Bullet 3415 */ 3416 var Bullet = function () { 3417 var self = this; 3418 3419 /** 3420 * toggle ordered list 3421 */ 3422 this.insertOrderedList = function (editable) { 3423 this.toggleList('OL', editable); 3424 }; 3425 3426 /** 3427 * toggle unordered list 3428 */ 3429 this.insertUnorderedList = function (editable) { 3430 this.toggleList('UL', editable); 3431 }; 3432 3433 /** 3434 * indent 3435 */ 3436 this.indent = function (editable) { 3437 var self = this; 3438 var rng = range.create(editable).wrapBodyInlineWithPara(); 3439 3440 var paras = rng.nodes(dom.isPara, { includeAncestor: true }); 3441 var clustereds = list.clusterBy(paras, func.peq2('parentNode')); 3442 3443 $.each(clustereds, function (idx, paras) { 3444 var head = list.head(paras); 3445 if (dom.isLi(head)) { 3446 self.wrapList(paras, head.parentNode.nodeName); 3447 } else { 3448 $.each(paras, function (idx, para) { 3449 $(para).css('marginLeft', function (idx, val) { 3450 return (parseInt(val, 10) || 0) + 25; 3451 }); 3452 }); 3453 } 3454 }); 3455 3456 rng.select(); 3457 }; 3458 3459 /** 3460 * outdent 3461 */ 3462 this.outdent = function (editable) { 3463 var self = this; 3464 var rng = range.create(editable).wrapBodyInlineWithPara(); 3465 3466 var paras = rng.nodes(dom.isPara, { includeAncestor: true }); 3467 var clustereds = list.clusterBy(paras, func.peq2('parentNode')); 3468 3469 $.each(clustereds, function (idx, paras) { 3470 var head = list.head(paras); 3471 if (dom.isLi(head)) { 3472 self.releaseList([paras]); 3473 } else { 3474 $.each(paras, function (idx, para) { 3475 $(para).css('marginLeft', function (idx, val) { 3476 val = (parseInt(val, 10) || 0); 3477 return val > 25 ? val - 25 : ''; 3478 }); 3479 }); 3480 } 3481 }); 3482 3483 rng.select(); 3484 }; 3485 3486 /** 3487 * toggle list 3488 * 3489 * @param {String} listName - OL or UL 3490 */ 3491 this.toggleList = function (listName, editable) { 3492 var rng = range.create(editable).wrapBodyInlineWithPara(); 3493 3494 var paras = rng.nodes(dom.isPara, { includeAncestor: true }); 3495 var bookmark = rng.paraBookmark(paras); 3496 var clustereds = list.clusterBy(paras, func.peq2('parentNode')); 3497 3498 // paragraph to list 3499 if (list.find(paras, dom.isPurePara)) { 3500 var wrappedParas = []; 3501 $.each(clustereds, function (idx, paras) {
3502 wrappedParas = wrappedParas.concat(self.wrapList(paras, listName)); 3503 }); 3504 paras = wrappedParas; 3505 // list to paragraph or change list style 3506 } else { 3507 var diffLists = rng.nodes(dom.isList, { 3508 includeAncestor: true 3509 }).filter(function (listNode) { 3510 return !$.nodeName(listNode, listName); 3511 }); 3512 3513 if (diffLists.length) { 3514 $.each(diffLists, function (idx, listNode) { 3515 dom.replace(listNode, listName); 3516 }); 3517 } else { 3518 paras = this.releaseList(clustereds, true); 3519 } 3520 } 3521 3522 range.createFromParaBookmark(bookmark, paras).select(); 3523 }; 3524 3525 /** 3526 * @param {Node[]} paras 3527 * @param {String} listName 3528 * @return {Node[]} 3529 */ 3530 this.wrapList = function (paras, listName) { 3531 var head = list.head(paras); 3532 var last = list.last(paras); 3533 3534 var prevList = dom.isList(head.previousSibling) && head.previousSibling; 3535 var nextList = dom.isList(last.nextSibling) && last.nextSibling; 3536 3537 var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last); 3538 3539 // P to LI 3540 paras = paras.map(function (para) { 3541 return dom.isPurePara(para) ? dom.replace(para, 'LI') : para; 3542 }); 3543 3544 // append to list(<ul>, <ol>) 3545 dom.appendChildNodes(listNode, paras); 3546 3547 if (nextList) { 3548 dom.appendChildNodes(listNode, list.from(nextList.childNodes)); 3549 dom.remove(nextList); 3550 } 3551 3552 return paras; 3553 }; 3554 3555 /** 3556 * @method releaseList 3557 * 3558 * @param {Array[]} clustereds 3559 * @param {Boolean} isEscapseToBody 3560 * @return {Node[]} 3561 */ 3562 this.releaseList = function (clustereds, isEscapseToBody) { 3563 var releasedParas = []; 3564 3565 $.each(clustereds, function (idx, paras) { 3566 var head = list.head(paras); 3567 var last = list.last(paras); 3568 3569 var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) : 3570 head.parentNode; 3571 var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, { 3572 node: last.parentNode, 3573 offset: dom.position(last) + 1 3574 }, { 3575 isSkipPaddingBlankHTML: true 3576 }) : null; 3577 3578 var middleList = dom.splitTree(headList, { 3579 node: head.parentNode, 3580 offset: dom.position(head) 3581 }, { 3582 isSkipPaddingBlankHTML: true 3583 }); 3584 3585 paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) : 3586 list.from(middleList.childNodes).filter(dom.isLi); 3587 3588 // LI to P 3589 if (isEscapseToBody || !dom.isList(headList.parentNode)) { 3590 paras = paras.map(function (para) { 3591 return dom.replace(para, 'P'); 3592 }); 3593 } 3594 3595 $.each(list.from(paras).reverse(), function (idx, para) { 3596 dom.insertAfter(para, headList); 3597 }); 3598 3599 // remove empty lists 3600 var rootLists = list.compact([headList, middleList, lastList]); 3601 $.each(rootLists, function (idx, rootList) { 3602 var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList)); 3603 $.each(listNodes.reverse(), function (idx, listNode) { 3604 if (!dom.nodeLength(listNode)) { 3605 dom.remove(listNode, true); 3606 } 3607 }); 3608 }); 3609 3610 releasedParas = releasedParas.concat(paras); 3611 }); 3612 3613 return releasedParas; 3614 }; 3615 }; 3616 3617 3618 /** 3619 * @class editing.Typing 3620 * 3621 * Typing 3622 * 3623 */ 3624 var Typing = function () { 3625 3626 // a Bullet instance to toggle lists off 3627 var bullet = new Bullet(); 3628 3629 /** 3630 * insert tab 3631 * 3632 * @param {WrappedRange} rng 3633 * @param {Number} tabsize 3634 */ 3635 this.insertTab = function (rng, tabsize) { 3636 var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR)); 3637 rng = rng.deleteContents(); 3638 rng.insertNode(tab, true); 3639 3640 rng = range.create(tab, tabsize); 3641 rng.select(); 3642 }; 3643 3644 /** 3645 * insert paragraph 3646 */ 3647 this.insertParagraph = function (editable) { 3648 var rng = range.create(editable); 3649 3650 // deleteContents on range. 3651 rng = rng.deleteContents(); 3652 3653 // Wrap range if it needs to be wrapped by paragraph 3654 rng = rng.wrapBodyInlineWithPara(); 3655 3656 // finding paragraph 3657 var splitRoot = dom.ancestor(rng.sc, dom.isPara); 3658 3659 var nextPara; 3660 // on paragraph: split paragraph 3661 if (splitRoot) { 3662 // if it is an empty line with li 3663 if (dom.isEmpty(splitRoot) && dom.isLi(splitRoot)) { 3664 // toogle UL/OL and escape 3665 bullet.toggleList(splitRoot.parentNode.nodeName); 3666 return; 3667 // if it is an empty line with para on blockquote 3668 } else if (dom.isEmpty(splitRoot) && dom.isPara(splitRoot) && dom.isBlockquote(splitRoot.parentNode)) { 3669 // escape blockquote 3670 dom.insertAfter(splitRoot, splitRoot.parentNode); 3671 nextPara = splitRoot; 3672 // if new line has content (not a line break) 3673 } else { 3674 nextPara = dom.splitTree(splitRoot, rng.getStartPoint()); 3675 3676 var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor); 3677 emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor)); 3678 3679 $.each(emptyAnchors, function (idx, anchor) { 3680 dom.remove(anchor); 3681 }); 3682 3683 // replace empty heading or pre with P tag 3684 if ((dom.isHeading(nextPara) || dom.isPre(nextPara)) && dom.isEmpty(nextPara)) { 3685 nextPara = dom.replace(nextPara, 'p'); 3686 } 3687 }
3688 // no paragraph: insert empty paragraph 3689 } else { 3690 var next = rng.sc.childNodes[rng.so]; 3691 nextPara = $(dom.emptyPara)[0]; 3692 if (next) { 3693 rng.sc.insertBefore(nextPara, next); 3694 } else { 3695 rng.sc.appendChild(nextPara); 3696 } 3697 } 3698 3699 range.create(nextPara, 0).normalize().select().scrollIntoView(editable); 3700 }; 3701 }; 3702 3703 /** 3704 * @class editing.Table 3705 * 3706 * Table 3707 * 3708 */ 3709 var Table = function () { 3710 /** 3711 * handle tab key 3712 * 3713 * @param {WrappedRange} rng 3714 * @param {Boolean} isShift 3715 */ 3716 this.tab = function (rng, isShift) { 3717 var cell = dom.ancestor(rng.commonAncestor(), dom.isCell); 3718 var table = dom.ancestor(cell, dom.isTable); 3719 var cells = dom.listDescendant(table, dom.isCell); 3720 3721 var nextCell = list[isShift ? 'prev' : 'next'](cells, cell); 3722 if (nextCell) { 3723 range.create(nextCell, 0).select(); 3724 } 3725 }; 3726 3727 /** 3728 * create empty table element 3729 * 3730 * @param {Number} rowCount 3731 * @param {Number} colCount 3732 * @return {Node} 3733 */ 3734 this.createTable = function (colCount, rowCount, options) { 3735 var tds = [], tdHTML; 3736 for (var idxCol = 0; idxCol < colCount; idxCol++) { 3737 tds.push('<td>' + dom.blank + '</td>'); 3738 } 3739 tdHTML = tds.join(''); 3740 3741 var trs = [], trHTML; 3742 for (var idxRow = 0; idxRow < rowCount; idxRow++) { 3743 trs.push('<tr>' + tdHTML + '</tr>'); 3744 } 3745 trHTML = trs.join(''); 3746 var $table = $('<table>' + trHTML + '</table>'); 3747 if (options && options.tableClassName) { 3748 $table.addClass(options.tableClassName); 3749 } 3750 3751 return $table[0]; 3752 }; 3753 }; 3754 3755 3756 var KEY_BOGUS = 'bogus'; 3757 3758 /** 3759 * @class Editor 3760 */ 3761 var Editor = function (context) { 3762 var self = this; 3763 3764 var $note = context.layoutInfo.note; 3765 var $editor = context.layoutInfo.editor; 3766 var $editable = context.layoutInfo.editable; 3767 var options = context.options; 3768 var lang = options.langInfo; 3769 3770 var editable = $editable[0]; 3771 var lastRange = null; 3772 3773 var style = new Style(); 3774 var table = new Table(); 3775 var typing = new Typing(); 3776 var bullet = new Bullet(); 3777 var history = new History($editable); 3778 3779 this.initialize = function () { 3780 // bind custom events 3781 $editable.on('keydown', function (event) { 3782 if (event.keyCode === key.code.ENTER) { 3783 context.triggerEvent('enter', event); 3784 } 3785 context.triggerEvent('keydown', event); 3786 3787 if (!event.isDefaultPrevented()) { 3788 if (options.shortcuts) { 3789 self.handleKeyMap(event); 3790 } else { 3791 self.preventDefaultEditableShortCuts(event); 3792 } 3793 } 3794 }).on('keyup', function (event) { 3795 context.triggerEvent('keyup', event); 3796 }).on('focus', function (event) { 3797 context.triggerEvent('focus', event); 3798 }).on('blur', function (event) { 3799 context.triggerEvent('blur', event); 3800 }).on('mousedown', function (event) { 3801 context.triggerEvent('mousedown', event); 3802 }).on('mouseup', function (event) { 3803 context.triggerEvent('mouseup', event); 3804 }).on('scroll', function (event) { 3805 context.triggerEvent('scroll', event); 3806 }).on('paste', function (event) { 3807 context.triggerEvent('paste', event); 3808 }); 3809 3810 // init content before set event 3811 $editable.html(dom.html($note) || dom.emptyPara); 3812 3813 // [workaround] IE doesn't have input events for contentEditable 3814 // - see: https://goo.gl/4bfIvA 3815 var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input'; 3816 $editable.on(changeEventName, func.debounce(function () { 3817 context.triggerEvent('change', $editable.html()); 3818 }, 250)); 3819 3820 $editor.on('focusin', function (event) { 3821 context.triggerEvent('focusin', event); 3822 }).on('focusout', function (event) { 3823 context.triggerEvent('focusout', event); 3824 }); 3825 3826 if (!options.airMode) { 3827 if (options.width) { 3828 $editor.outerWidth(options.width); 3829 } 3830 if (options.height) { 3831 $editable.outerHeight(options.height); 3832 } 3833 if (options.maxHeight) { 3834 $editable.css('max-height', options.maxHeight); 3835 } 3836 if (options.minHeight) { 3837 $editable.css('min-height', options.minHeight); 3838 } 3839 } 3840 3841 history.recordUndo(); 3842 }; 3843 3844 this.destroy = function () { 3845 $editable.off(); 3846 }; 3847 3848 this.handleKeyMap = function (event) { 3849 var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc']; 3850 var keys = []; 3851 3852 if (event.metaKey) { keys.push('CMD'); } 3853 if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); } 3854 if (event.shiftKey) { keys.push('SHIFT'); } 3855 3856 var keyName = key.nameFromCode[event.keyCode]; 3857 if (keyName) { 3858 keys.push(keyName); 3859 } 3860 3861 var eventName = keyMap[keys.join('+')]; 3862 if (eventName) { 3863 event.preventDefault();
3864 context.invoke(eventName); 3865 } else if (key.isEdit(event.keyCode)) { 3866 this.afterCommand(); 3867 } 3868 }; 3869 3870 this.preventDefaultEditableShortCuts = function (event) { 3871 // B(Bold, 66) / I(Italic, 73) / U(Underline, 85) 3872 if ((event.ctrlKey || event.metaKey) && 3873 list.contains([66, 73, 85], event.keyCode)) { 3874 event.preventDefault(); 3875 } 3876 }; 3877 3878 /** 3879 * create range 3880 * @return {WrappedRange} 3881 */ 3882 this.createRange = function () { 3883 this.focus(); 3884 return range.create(editable); 3885 }; 3886 3887 /** 3888 * saveRange 3889 * 3890 * save current range 3891 * 3892 * @param {Boolean} [thenCollapse=false] 3893 */ 3894 this.saveRange = function (thenCollapse) { 3895 lastRange = this.createRange(); 3896 if (thenCollapse) { 3897 lastRange.collapse().select(); 3898 } 3899 }; 3900 3901 /** 3902 * restoreRange 3903 * 3904 * restore lately range 3905 */ 3906 this.restoreRange = function () { 3907 if (lastRange) { 3908 lastRange.select(); 3909 this.focus(); 3910 } 3911 }; 3912 3913 this.saveTarget = function (node) { 3914 $editable.data('target', node); 3915 }; 3916 3917 this.clearTarget = function () { 3918 $editable.removeData('target'); 3919 }; 3920 3921 this.restoreTarget = function () { 3922 return $editable.data('target'); 3923 }; 3924 3925 /** 3926 * currentStyle 3927 * 3928 * current style 3929 * @return {Object|Boolean} unfocus 3930 */ 3931 this.currentStyle = function () { 3932 var rng = range.create(); 3933 if (rng) { 3934 rng = rng.normalize(); 3935 } 3936 return rng ? style.current(rng) : style.fromNode($editable); 3937 }; 3938 3939 /** 3940 * style from node 3941 * 3942 * @param {jQuery} $node 3943 * @return {Object} 3944 */ 3945 this.styleFromNode = function ($node) { 3946 return style.fromNode($node); 3947 }; 3948 3949 /** 3950 * undo 3951 */ 3952 this.undo = function () { 3953 context.triggerEvent('before.command', $editable.html()); 3954 history.undo(); 3955 context.triggerEvent('change', $editable.html()); 3956 }; 3957 context.memo('help.undo', lang.help.undo); 3958 3959 /** 3960 * redo 3961 */ 3962 this.redo = function () { 3963 context.triggerEvent('before.command', $editable.html()); 3964 history.redo(); 3965 context.triggerEvent('change', $editable.html()); 3966 }; 3967 context.memo('help.redo', lang.help.redo); 3968 3969 /** 3970 * before command 3971 */ 3972 var beforeCommand = this.beforeCommand = function () { 3973 context.triggerEvent('before.command', $editable.html()); 3974 // keep focus on editable before command execution 3975 self.focus(); 3976 }; 3977 3978 /** 3979 * after command 3980 * @param {Boolean} isPreventTrigger 3981 */ 3982 var afterCommand = this.afterCommand = function (isPreventTrigger) { 3983 history.recordUndo(); 3984 if (!isPreventTrigger) { 3985 context.triggerEvent('change', $editable.html()); 3986 } 3987 }; 3988 3989 /* jshint ignore:start */ 3990 // native commands(with execCommand), generate function for execCommand 3991 var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 3992 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', 3993 'formatBlock', 'removeFormat', 3994 'backColor', 'foreColor', 'fontName']; 3995 3996 for (var idx = 0, len = commands.length; idx < len; idx ++) { 3997 this[commands[idx]] = (function (sCmd) { 3998 return function (value) { 3999 beforeCommand(); 4000 document.execCommand(sCmd, false, value); 4001 afterCommand(true); 4002 }; 4003 })(commands[idx]); 4004 context.memo('help.' + commands[idx], lang.help[commands[idx]]); 4005 } 4006 /* jshint ignore:end */ 4007 4008 /** 4009 * handle tab key 4010 */ 4011 this.tab = function () { 4012 var rng = this.createRange(); 4013 if (rng.isCollapsed() && rng.isOnCell()) { 4014 table.tab(rng); 4015 } else { 4016 beforeCommand(); 4017 typing.insertTab(rng, options.tabSize); 4018 afterCommand(); 4019 } 4020 }; 4021 context.memo('help.tab', lang.help.tab); 4022 4023 /** 4024 * handle shift+tab key 4025 */ 4026 this.untab = function () { 4027 var rng = this.createRange(); 4028 if (rng.isCollapsed() && rng.isOnCell()) { 4029 table.tab(rng, true); 4030 } 4031 }; 4032 context.memo('help.untab', lang.help.untab); 4033 4034 /** 4035 * run given function between beforeCommand and afterCommand 4036 */ 4037 this.wrapCommand = function (fn) { 4038 return function () { 4039 beforeCommand(); 4040 fn.apply(self, arguments); 4041 afterCommand(); 4042 }; 4043 }; 4044 4045 /** 4046 * insert paragraph 4047 */ 4048 this.insertParagraph = this.wrapCommand(function () { 4049 typing.insertParagraph(editable); 4050 }); 4051 context.memo('help.insertParagraph', lang.help.insertParagraph); 4052 4053 this.insertOrderedList = this.wrapCommand(function () { 4054 bullet.insertOrderedList(editable); 4055 }); 4056 context.memo('help.insertOrderedList', lang.help.insertOrderedList); 4057 4058 this.insertUnorderedList = this.wrapCommand(function () { 4059 bullet.insertUnorderedList(editable); 4060 }); 4061 context.memo('help.insertUnorderedList', lang.help.insertUnorderedList); 4062 4063 this.indent = this.wrapCommand(function () { 4064 bullet.indent(editable); 4065 }); 4066 context.memo('help.indent', lang.help.indent); 4067 4068 this.outdent = this.wrapCommand(function () { 4069 bullet.outdent(editable); 4070 }); 4071 context.memo('help.outdent', lang.help.outdent); 4072 4073 /** 4074 * insert image 4075 * 4076 * @param {String} src 4077 * @param {String|Function} param 4078 * @return {Promise} 4079 */ 4080 this.insertImage = function (src, param) { 4081 return async.createImage(src, param).then(function ($image) { 4082 beforeCommand(); 4083 4084 if (typeof param === 'function') { 4085 param($image); 4086 } else { 4087 if (typeof param === 'string') { 4088 $image.attr('data-filename', param); 4089 } 4090 $image.css('width', Math.min($editable.width(), $image.width())); 4091 } 4092 4093 $image.show(); 4094 range.create(editable).insertNode($image[0]); 4095 range.createFromNodeAfter($image[0]).select(); 4096 afterCommand(); 4097 }).fail(function (e) { 4098 context.triggerEvent('image.upload.error', e); 4099 }); 4100 }; 4101 4102 /** 4103 * insertImages 4104 * @param {File[]} files 4105 */ 4106 this.insertImages = function (files) { 4107 $.each(files, function (idx, file) { 4108 var filename = file.name; 4109 if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) { 4110 context.triggerEvent('image.upload.error', lang.image.maximumFileSizeError); 4111 } else { 4112 async.readFileAsDataURL(file).then(function (dataURL) { 4113 return self.insertImage(dataURL, filename); 4114 }).fail(function () { 4115 context.triggerEvent('image.upload.error'); 4116 }); 4117 } 4118 }); 4119 }; 4120 4121 /** 4122 * insertImagesOrCallback 4123 * @param {File[]} files 4124 */ 4125 this.insertImagesOrCallback = function (files) { 4126 var callbacks = options.callbacks; 4127 4128 // If onImageUpload options setted 4129 if (callbacks.onImageUpload) { 4130 context.triggerEvent('image.upload', files); 4131 // else insert Image as dataURL 4132 } else { 4133 this.insertImages(files); 4134 } 4135 }; 4136 4137 /**
4138 * insertNode 4139 * insert node 4140 * @param {Node} node 4141 */ 4142 this.insertNode = this.wrapCommand(function (node) { 4143 var rng = this.createRange(); 4144 rng.insertNode(node); 4145 range.createFromNodeAfter(node).select(); 4146 }); 4147 4148 /** 4149 * insert text 4150 * @param {String} text 4151 */ 4152 this.insertText = this.wrapCommand(function (text) { 4153 var rng = this.createRange(); 4154 var textNode = rng.insertNode(dom.createText(text)); 4155 range.create(textNode, dom.nodeLength(textNode)).select(); 4156 }); 4157 4158 /** 4159 * return selected plain text 4160 * @return {String} text 4161 */ 4162 this.getSelectedText = function () { 4163 var rng = this.createRange(); 4164 4165 // if range on anchor, expand range with anchor 4166 if (rng.isOnAnchor()) { 4167 rng = range.createFromNode(dom.ancestor(rng.sc, dom.isAnchor)); 4168 } 4169 4170 return rng.toString(); 4171 }; 4172 4173 /** 4174 * paste HTML 4175 * @param {String} markup 4176 */ 4177 this.pasteHTML = this.wrapCommand(function (markup) { 4178 var contents = this.createRange().pasteHTML(markup); 4179 range.createFromNodeAfter(list.last(contents)).select(); 4180 }); 4181 4182 /** 4183 * formatBlock 4184 * 4185 * @param {String} tagName 4186 */ 4187 this.formatBlock = this.wrapCommand(function (tagName) { 4188 // [workaround] for MSIE, IE need `<` 4189 tagName = agent.isMSIE ? '<' + tagName + '>' : tagName; 4190 document.execCommand('FormatBlock', false, tagName); 4191 }); 4192 4193 this.formatPara = function () { 4194 this.formatBlock('P'); 4195 }; 4196 context.memo('help.formatPara', lang.help.formatPara); 4197 4198 /* jshint ignore:start */ 4199 for (var idx = 1; idx <= 6; idx ++) { 4200 this['formatH' + idx] = function (idx) { 4201 return function () { 4202 this.formatBlock('H' + idx); 4203 }; 4204 }(idx); 4205 context.memo('help.formatH'+idx, lang.help['formatH' + idx]); 4206 }; 4207 /* jshint ignore:end */ 4208 4209 /** 4210 * fontSize 4211 * 4212 * @param {String} value - px 4213 */ 4214 this.fontSize = function (value) { 4215 var rng = this.createRange(); 4216 4217 if (rng && rng.isCollapsed()) { 4218 var spans = style.styleNodes(rng); 4219 var firstSpan = list.head(spans); 4220 4221 $(spans).css({ 4222 'font-size': value + 'px' 4223 }); 4224 4225 // [workaround] added styled bogus span for style 4226 // - also bogus character needed for cursor position 4227 if (firstSpan && !dom.nodeLength(firstSpan)) { 4228 firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR; 4229 range.createFromNodeAfter(firstSpan.firstChild).select(); 4230 $editable.data(KEY_BOGUS, firstSpan); 4231 } 4232 } else { 4233 beforeCommand(); 4234 $(style.styleNodes(rng)).css({ 4235 'font-size': value + 'px' 4236 }); 4237 afterCommand(); 4238 } 4239 }; 4240 4241 /** 4242 * insert horizontal rule 4243 */ 4244 this.insertHorizontalRule = this.wrapCommand(function () { 4245 var hrNode = this.createRange().insertNode(dom.create('HR')); 4246 if (hrNode.nextSibling) { 4247 range.create(hrNode.nextSibling, 0).normalize().select(); 4248 } 4249 }); 4250 context.memo('help.insertHorizontalRule', lang.help.insertHorizontalRule); 4251 4252 /** 4253 * remove bogus node and character 4254 */ 4255 this.removeBogus = function () { 4256 var bogusNode = $editable.data(KEY_BOGUS); 4257 if (!bogusNode) { 4258 return; 4259 } 4260 4261 var textNode = list.find(list.from(bogusNode.childNodes), dom.isText); 4262 4263 var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR); 4264 if (bogusCharIdx !== -1) { 4265 textNode.deleteData(bogusCharIdx, 1); 4266 } 4267 4268 if (dom.isEmpty(bogusNode)) { 4269 dom.remove(bogusNode); 4270 } 4271 4272 $editable.removeData(KEY_BOGUS); 4273 }; 4274 4275 /** 4276 * lineHeight 4277 * @param {String} value 4278 */ 4279 this.lineHeight = this.wrapCommand(function (value) { 4280 style.stylePara(this.createRange(), { 4281 lineHeight: value 4282 }); 4283 }); 4284 4285 /** 4286 * unlink 4287 * 4288 * @type command 4289 */ 4290 this.unlink = function () { 4291 var rng = this.createRange(); 4292 if (rng.isOnAnchor()) { 4293 var anchor = dom.ancestor(rng.sc, dom.isAnchor); 4294 rng = range.createFromNode(anchor); 4295 rng.select(); 4296 4297 beforeCommand(); 4298 document.execCommand('unlink'); 4299 afterCommand(); 4300 } 4301 }; 4302 4303 /** 4304 * create link (command) 4305 * 4306 * @param {Object} linkInfo 4307 */ 4308 this.createLink = this.wrapCommand(function (linkInfo) { 4309 var linkUrl = linkInfo.url; 4310 var linkText = linkInfo.text; 4311 var isNewWindow = linkInfo.isNewWindow; 4312 var rng = linkInfo.range || this.createRange(); 4313 var isTextChanged = rng.toString() !== linkText; 4314 4315 // handle spaced urls from input 4316 if (typeof linkUrl === 'string') { 4317 linkUrl = linkUrl.trim(); 4318 } 4319 4320 if (options.onCreateLink) { 4321 linkUrl = options.onCreateLink(linkUrl); 4322 } 4323 4324 var anchors = []; 4325 if (isTextChanged) { 4326 rng = rng.deleteContents();
4327 var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0]); 4328 anchors.push(anchor); 4329 } else { 4330 anchors = style.styleNodes(rng, { 4331 nodeName: 'A', 4332 expandClosestSibling: true, 4333 onlyPartialContains: true 4334 }); 4335 } 4336 4337 $.each(anchors, function (idx, anchor) { 4338 // if url doesn't match an URL schema, set http:// as default 4339 linkUrl = /^[A-Za-z][A-Za-z0-9+-.]*\:[\/\/]?/.test(linkUrl) ? 4340 linkUrl : 'http://' + linkUrl; 4341 4342 $(anchor).attr('href', linkUrl); 4343 if (isNewWindow) { 4344 $(anchor).attr('target', '_blank'); 4345 } else { 4346 $(anchor).removeAttr('target'); 4347 } 4348 }); 4349 4350 var startRange = range.createFromNodeBefore(list.head(anchors)); 4351 var startPoint = startRange.getStartPoint(); 4352 var endRange = range.createFromNodeAfter(list.last(anchors)); 4353 var endPoint = endRange.getEndPoint(); 4354 4355 range.create( 4356 startPoint.node, 4357 startPoint.offset, 4358 endPoint.node, 4359 endPoint.offset 4360 ).select(); 4361 }); 4362 4363 /** 4364 * returns link info 4365 * 4366 * @return {Object} 4367 * @return {WrappedRange} return.range 4368 * @return {String} return.text 4369 * @return {Boolean} [return.isNewWindow=true] 4370 * @return {String} [return.url=""] 4371 */ 4372 this.getLinkInfo = function () { 4373 var rng = this.createRange().expand(dom.isAnchor); 4374 4375 // Get the first anchor on range(for edit). 4376 var $anchor = $(list.head(rng.nodes(dom.isAnchor))); 4377 4378 return { 4379 range: rng, 4380 text: rng.toString(), 4381 isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : false, 4382 url: $anchor.length ? $anchor.attr('href') : '' 4383 }; 4384 }; 4385 4386 /** 4387 * setting color 4388 * 4389 * @param {Object} sObjColor color code 4390 * @param {String} sObjColor.foreColor foreground color 4391 * @param {String} sObjColor.backColor background color 4392 */ 4393 this.color = this.wrapCommand(function (colorInfo) { 4394 var foreColor = colorInfo.foreColor; 4395 var backColor = colorInfo.backColor; 4396 4397 if (foreColor) { document.execCommand('foreColor', false, foreColor); } 4398 if (backColor) { document.execCommand('backColor', false, backColor); } 4399 }); 4400 4401 /** 4402 * insert Table 4403 * 4404 * @param {String} dimension of table (ex : "5x5") 4405 */ 4406 this.insertTable = this.wrapCommand(function (dim) { 4407 var dimension = dim.split('x'); 4408 4409 var rng = this.createRange().deleteContents(); 4410 rng.insertNode(table.createTable(dimension[0], dimension[1], options)); 4411 }); 4412 4413 /** 4414 * float me 4415 * 4416 * @param {String} value 4417 */ 4418 this.floatMe = this.wrapCommand(function (value) { 4419 var $target = $(this.restoreTarget()); 4420 $target.css('float', value); 4421 }); 4422 4423 /** 4424 * resize overlay element 4425 * @param {String} value 4426 */ 4427 this.resize = this.wrapCommand(function (value) { 4428 var $target = $(this.restoreTarget()); 4429 $target.css({ 4430 width: value * 100 + '%', 4431 height: '' 4432 }); 4433 }); 4434 4435 /** 4436 * @param {Position} pos 4437 * @param {jQuery} $target - target element 4438 * @param {Boolean} [bKeepRatio] - keep ratio 4439 */ 4440 this.resizeTo = function (pos, $target, bKeepRatio) { 4441 var imageSize; 4442 if (bKeepRatio) { 4443 var newRatio = pos.y / pos.x; 4444 var ratio = $target.data('ratio'); 4445 imageSize = { 4446 width: ratio > newRatio ? pos.x : pos.y / ratio, 4447 height: ratio > newRatio ? pos.x * ratio : pos.y 4448 }; 4449 } else { 4450 imageSize = { 4451 width: pos.x, 4452 height: pos.y 4453 }; 4454 } 4455 4456 $target.css(imageSize); 4457 }; 4458 4459 /** 4460 * remove media object 4461 */ 4462 this.removeMedia = this.wrapCommand(function () { 4463 var $target = $(this.restoreTarget()).detach(); 4464 context.triggerEvent('media.delete', $target, $editable); 4465 }); 4466 4467 /** 4468 * returns whether editable area has focus or not. 4469 */ 4470 this.hasFocus = function () { 4471 return $editable.is(':focus'); 4472 }; 4473 4474 /** 4475 * set focus 4476 */ 4477 this.focus = function () { 4478 // [workaround] Screen will move when page is scolled in IE. 4479 // - do focus when not focused 4480 if (!this.hasFocus()) { 4481 $editable.focus(); 4482 } 4483 }; 4484 4485 /** 4486 * returns whether contents is empty or not. 4487 * @return {Boolean} 4488 */ 4489 this.isEmpty = function () { 4490 return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html(); 4491 }; 4492 4493 /** 4494 * Removes all contents and restores the editable instance to an _emptyPara_. 4495 */ 4496 this.empty = function () {
4497 context.invoke('code', dom.emptyPara); 4498 }; 4499 }; 4500 4501 var Clipboard = function (context) { 4502 var self = this; 4503 4504 var $editable = context.layoutInfo.editable; 4505 4506 this.events = { 4507 'summernote.keydown': function (we, e) { 4508 if (self.needKeydownHook()) { 4509 if ((e.ctrlKey || e.metaKey) && e.keyCode === key.code.V) { 4510 context.invoke('editor.saveRange'); 4511 self.$paste.focus(); 4512 4513 setTimeout(function () { 4514 self.pasteByHook(); 4515 }, 0); 4516 } 4517 } 4518 } 4519 }; 4520 4521 this.needKeydownHook = function () { 4522 return (agent.isMSIE && agent.browserVersion > 10) || agent.isFF; 4523 }; 4524 4525 this.initialize = function () { 4526 // [workaround] getting image from clipboard 4527 // - IE11 and Firefox: CTRL+v hook 4528 // - Webkit: event.clipboardData 4529 if (this.needKeydownHook()) { 4530 this.$paste = $('<div tabindex="-1" />').attr('contenteditable', true).css({ 4531 position: 'absolute', 4532 left: -100000, 4533 opacity: 0 4534 }); 4535 $editable.before(this.$paste); 4536 4537 this.$paste.on('paste', function (event) { 4538 context.triggerEvent('paste', event); 4539 }); 4540 } else { 4541 $editable.on('paste', this.pasteByEvent); 4542 } 4543 }; 4544 4545 this.destroy = function () { 4546 if (this.needKeydownHook()) { 4547 this.$paste.remove(); 4548 this.$paste = null; 4549 } 4550 }; 4551 4552 this.pasteByHook = function () { 4553 var node = this.$paste[0].firstChild; 4554 4555 if (dom.isImg(node)) { 4556 var dataURI = node.src; 4557 var decodedData = atob(dataURI.split(',')[1]); 4558 var array = new Uint8Array(decodedData.length); 4559 for (var i = 0; i < decodedData.length; i++) { 4560 array[i] = decodedData.charCodeAt(i); 4561 } 4562 4563 var blob = new Blob([array], { type: 'image/png' }); 4564 blob.name = 'clipboard.png'; 4565 4566 context.invoke('editor.restoreRange'); 4567 context.invoke('editor.focus'); 4568 context.invoke('editor.insertImagesOrCallback', [blob]); 4569 } else { 4570 var pasteContent = $('<div />').html(this.$paste.html()).html(); 4571 context.invoke('editor.restoreRange'); 4572 context.invoke('editor.focus'); 4573 4574 if (pasteContent) { 4575 context.invoke('editor.pasteHTML', pasteContent); 4576 } 4577 } 4578 4579 this.$paste.empty(); 4580 }; 4581 4582 /** 4583 * paste by clipboard event 4584 * 4585 * @param {Event} event 4586 */ 4587 this.pasteByEvent = function (event) { 4588 var clipboardData = event.originalEvent.clipboardData; 4589 if (clipboardData && clipboardData.items && clipboardData.items.length) { 4590 var item = list.head(clipboardData.items); 4591 if (item.kind === 'file' && item.type.indexOf('image/') !== -1) { 4592 context.invoke('editor.insertImagesOrCallback', [item.getAsFile()]); 4593 } 4594 context.invoke('editor.afterCommand'); 4595 } 4596 }; 4597 }; 4598 4599 var Dropzone = function (context) { 4600 var $document = $(document); 4601 var $editor = context.layoutInfo.editor; 4602 var $editable = context.layoutInfo.editable; 4603 var options = context.options; 4604 var lang = options.langInfo; 4605 var documentEventHandlers = {}; 4606 4607 var $dropzone = $([ 4608 '<div class="note-dropzone">', 4609 ' <div class="note-dropzone-message"/>', 4610 '</div>' 4611 ].join('')).prependTo($editor); 4612 4613 var detachDocumentEvent = function () { 4614 Object.keys(documentEventHandlers).forEach(function (key) { 4615 $document.off(key.substr(2).toLowerCase(), documentEventHandlers[key]); 4616 }); 4617 documentEventHandlers = {}; 4618 }; 4619 4620 /** 4621 * attach Drag and Drop Events 4622 */ 4623 this.initialize = function () { 4624 if (options.disableDragAndDrop) { 4625 // prevent default drop event 4626 documentEventHandlers.onDrop = function (e) { 4627 e.preventDefault(); 4628 }; 4629 $document.on('drop', documentEventHandlers.onDrop); 4630 } else { 4631 this.attachDragAndDropEvent(); 4632 } 4633 }; 4634 4635 /** 4636 * attach Drag and Drop Events 4637 */ 4638 this.attachDragAndDropEvent = function () { 4639 var collection = $(), 4640 $dropzoneMessage = $dropzone.find('.note-dropzone-message'); 4641 4642 documentEventHandlers.onDragenter = function (e) {
4643 var isCodeview = context.invoke('codeview.isActivated'); 4644 var hasEditorSize = $editor.width() > 0 && $editor.height() > 0; 4645 if (!isCodeview && !collection.length && hasEditorSize) { 4646 $editor.addClass('dragover'); 4647 $dropzone.width($editor.width()); 4648 $dropzone.height($editor.height()); 4649 $dropzoneMessage.text(lang.image.dragImageHere); 4650 } 4651 collection = collection.add(e.target); 4652 }; 4653 4654 documentEventHandlers.onDragleave = function (e) { 4655 collection = collection.not(e.target); 4656 if (!collection.length) { 4657 $editor.removeClass('dragover'); 4658 } 4659 }; 4660 4661 documentEventHandlers.onDrop = function () { 4662 collection = $(); 4663 $editor.removeClass('dragover'); 4664 }; 4665 4666 // show dropzone on dragenter when dragging a object to document 4667 // -but only if the editor is visible, i.e. has a positive width and height 4668 $document.on('dragenter', documentEventHandlers.onDragenter) 4669 .on('dragleave', documentEventHandlers.onDragleave) 4670 .on('drop', documentEventHandlers.onDrop); 4671 4672 // change dropzone's message on hover. 4673 $dropzone.on('dragenter', function () { 4674 $dropzone.addClass('hover'); 4675 $dropzoneMessage.text(lang.image.dropImage); 4676 }).on('dragleave', function () { 4677 $dropzone.removeClass('hover'); 4678 $dropzoneMessage.text(lang.image.dragImageHere); 4679 }); 4680 4681 // attach dropImage 4682 $dropzone.on('drop', function (event) { 4683 var dataTransfer = event.originalEvent.dataTransfer; 4684 4685 if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { 4686 event.preventDefault(); 4687 $editable.focus(); 4688 context.invoke('editor.insertImagesOrCallback', dataTransfer.files); 4689 } else { 4690 $.each(dataTransfer.types, function (idx, type) { 4691 var content = dataTransfer.getData(type); 4692 4693 if (type.toLowerCase().indexOf('text') > -1) { 4694 context.invoke('editor.pasteHTML', content); 4695 } else { 4696 $(content).each(function () { 4697 context.invoke('editor.insertNode', this); 4698 }); 4699 } 4700 }); 4701 } 4702 }).on('dragover', false); // prevent default dragover event 4703 }; 4704 4705 this.destroy = function () { 4706 detachDocumentEvent(); 4707 }; 4708 }; 4709 4710 4711 var CodeMirror; 4712 if (agent.hasCodeMirror) { 4713 if (agent.isSupportAmd) { 4714 require(['codemirror'], function (cm) { 4715 CodeMirror = cm; 4716 }); 4717 } else { 4718 CodeMirror = window.CodeMirror; 4719 } 4720 } 4721 4722 /** 4723 * @class Codeview 4724 */ 4725 var Codeview = function (context) { 4726 var $editor = context.layoutInfo.editor; 4727 var $editable = context.layoutInfo.editable; 4728 var $codable = context.layoutInfo.codable; 4729 var options = context.options; 4730 4731 this.sync = function () { 4732 var isCodeview = this.isActivated(); 4733 if (isCodeview && agent.hasCodeMirror) { 4734 $codable.data('cmEditor').save(); 4735 } 4736 }; 4737 4738 /** 4739 * @return {Boolean} 4740 */ 4741 this.isActivated = function () { 4742 return $editor.hasClass('codeview'); 4743 }; 4744 4745 /** 4746 * toggle codeview 4747 */ 4748 this.toggle = function () { 4749 if (this.isActivated()) { 4750 this.deactivate(); 4751 } else { 4752 this.activate(); 4753 } 4754 context.triggerEvent('codeview.toggled'); 4755 }; 4756 4757 /** 4758 * activate code view 4759 */ 4760 this.activate = function () { 4761 $codable.val(dom.html($editable, options.prettifyHtml)); 4762 $codable.height($editable.height()); 4763 4764 context.invoke('toolbar.updateCodeview', true); 4765 $editor.addClass('codeview'); 4766 $codable.focus(); 4767 4768 // activate CodeMirror as codable 4769 if (agent.hasCodeMirror) { 4770 var cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror); 4771 4772 // CodeMirror TernServer 4773 if (options.codemirror.tern) { 4774 var server = new CodeMirror.TernServer(options.codemirror.tern); 4775 cmEditor.ternServer = server; 4776 cmEditor.on('cursorActivity', function (cm) { 4777 server.updateArgHints(cm); 4778 }); 4779 } 4780 4781 // CodeMirror hasn't Padding. 4782 cmEditor.setSize(null, $editable.outerHeight()); 4783 $codable.data('cmEditor', cmEditor); 4784 } 4785 }; 4786 4787 /** 4788 * deactivate code view 4789 */ 4790 this.deactivate = function () { 4791 // deactivate CodeMirror as codable 4792 if (agent.hasCodeMirror) { 4793 var cmEditor = $codable.data('cmEditor'); 4794 $codable.val(cmEditor.getValue()); 4795 cmEditor.toTextArea(); 4796 } 4797 4798 var value = dom.value($codable, options.prettifyHtml) || dom.emptyPara; 4799 var isChange = $editable.html() !== value; 4800 4801 $editable.html(value); 4802 $editable.height(options.height ? $codable.height() : 'auto'); 4803 $editor.removeClass('codeview'); 4804 4805 if (isChange) { 4806 context.triggerEvent('change', $editable.html(), $editable); 4807 } 4808 4809 $editable.focus(); 4810
4811 context.invoke('toolbar.updateCodeview', false); 4812 }; 4813 4814 this.destroy = function () { 4815 if (this.isActivated()) { 4816 this.deactivate(); 4817 } 4818 }; 4819 }; 4820 4821 var EDITABLE_PADDING = 24; 4822 4823 var Statusbar = function (context) { 4824 var $document = $(document); 4825 var $statusbar = context.layoutInfo.statusbar; 4826 var $editable = context.layoutInfo.editable; 4827 var options = context.options; 4828 4829 this.initialize = function () { 4830 if (options.airMode || options.disableResizeEditor) { 4831 return; 4832 } 4833 4834 $statusbar.on('mousedown', function (event) { 4835 event.preventDefault(); 4836 event.stopPropagation(); 4837 4838 var editableTop = $editable.offset().top - $document.scrollTop(); 4839 4840 $document.on('mousemove', function (event) { 4841 var height = event.clientY - (editableTop + EDITABLE_PADDING); 4842 4843 height = (options.minheight > 0) ? Math.max(height, options.minheight) : height; 4844 height = (options.maxHeight > 0) ? Math.min(height, options.maxHeight) : height; 4845 4846 $editable.height(height); 4847 }).one('mouseup', function () { 4848 $document.off('mousemove'); 4849 }); 4850 }); 4851 }; 4852 4853 this.destroy = function () { 4854 $statusbar.off(); 4855 $statusbar.remove(); 4856 }; 4857 }; 4858 4859 var Fullscreen = function (context) { 4860 var $editor = context.layoutInfo.editor; 4861 var $toolbar = context.layoutInfo.toolbar; 4862 var $editable = context.layoutInfo.editable; 4863 var $codable = context.layoutInfo.codable; 4864 4865 var $window = $(window); 4866 var $scrollbar = $('html, body'); 4867 4868 /** 4869 * toggle fullscreen 4870 */ 4871 this.toggle = function () { 4872 var resize = function (size) { 4873 $editable.css('height', size.h); 4874 $codable.css('height', size.h); 4875 if ($codable.data('cmeditor')) { 4876 $codable.data('cmeditor').setsize(null, size.h); 4877 } 4878 }; 4879 4880 $editor.toggleClass('fullscreen'); 4881 if (this.isFullscreen()) { 4882 $editable.data('orgHeight', $editable.css('height')); 4883 4884 $window.on('resize', function () { 4885 resize({ 4886 h: $window.height() - $toolbar.outerHeight() 4887 }); 4888 }).trigger('resize'); 4889 4890 $scrollbar.css('overflow', 'hidden'); 4891 } else { 4892 $window.off('resize'); 4893 resize({ 4894 h: $editable.data('orgHeight') 4895 }); 4896 $scrollbar.css('overflow', 'visible'); 4897 } 4898 4899 context.invoke('toolbar.updateFullscreen', this.isFullscreen()); 4900 }; 4901 4902 this.isFullscreen = function () { 4903 return $editor.hasClass('fullscreen'); 4904 }; 4905 }; 4906 4907 var Handle = function (context) { 4908 var self = this; 4909 4910 var $document = $(document); 4911 var $editingArea = context.layoutInfo.editingArea; 4912 var options = context.options; 4913 4914 this.events = { 4915 'summernote.mousedown': function (we, e) { 4916 if (self.update(e.target)) { 4917 e.preventDefault(); 4918 } 4919 }, 4920 'summernote.keyup summernote.scroll summernote.change summernote.dialog.shown': function () { 4921 self.update(); 4922 } 4923 }; 4924 4925 this.initialize = function () { 4926 this.$handle = $([ 4927 '<div class="note-handle">', 4928 '<div class="note-control-selection">', 4929 '<div class="note-control-selection-bg"></div>', 4930 '<div class="note-control-holder note-control-nw"></div>', 4931 '<div class="note-control-holder note-control-ne"></div>', 4932 '<div class="note-control-holder note-control-sw"></div>', 4933 '<div class="', 4934 (options.disableResizeImage ? 'note-control-holder' : 'note-control-sizing'), 4935 ' note-control-se"></div>', 4936 (options.disableResizeImage ? '' : '<div class="note-control-selection-info"></div>'), 4937 '</div>', 4938 '</div>' 4939 ].join('')).prependTo($editingArea); 4940 4941 this.$handle.on('mousedown', function (event) { 4942 if (dom.isControlSizing(event.target)) { 4943 event.preventDefault(); 4944 event.stopPropagation(); 4945 4946 var $target = self.$handle.find('.note-control-selection').data('target'), 4947 posStart = $target.offset(), 4948 scrollTop = $document.scrollTop(); 4949 4950 $document.on('mousemove', function (event) {
4951 context.invoke('editor.resizeTo', { 4952 x: event.clientX - posStart.left, 4953 y: event.clientY - (posStart.top - scrollTop) 4954 }, $target, !event.shiftKey); 4955 4956 self.update($target[0]); 4957 }).one('mouseup', function (e) { 4958 e.preventDefault(); 4959 $document.off('mousemove'); 4960 context.invoke('editor.afterCommand'); 4961 }); 4962 4963 if (!$target.data('ratio')) { // original ratio. 4964 $target.data('ratio', $target.height() / $target.width()); 4965 } 4966 } 4967 }); 4968 }; 4969 4970 this.destroy = function () { 4971 this.$handle.remove(); 4972 }; 4973 4974 this.update = function (target) { 4975 var isImage = dom.isImg(target); 4976 var $selection = this.$handle.find('.note-control-selection'); 4977 4978 context.invoke('imagePopover.update', target); 4979 4980 if (isImage) { 4981 var $image = $(target); 4982 var pos = $image.position(); 4983 4984 // include margin 4985 var imageSize = { 4986 w: $image.outerWidth(true), 4987 h: $image.outerHeight(true) 4988 }; 4989 4990 $selection.css({ 4991 display: 'block', 4992 left: pos.left, 4993 top: pos.top, 4994 width: imageSize.w, 4995 height: imageSize.h 4996 }).data('target', $image); // save current image element. 4997 4998 var sizingText = imageSize.w + 'x' + imageSize.h; 4999 $selection.find('.note-control-selection-info').text(sizingText); 5000 context.invoke('editor.saveTarget', target); 5001 } else { 5002 this.hide(); 5003 } 5004 5005 return isImage; 5006 }; 5007 5008 /** 5009 * hide 5010 * 5011 * @param {jQuery} $handle 5012 */ 5013 this.hide = function () { 5014 context.invoke('editor.clearTarget'); 5015 this.$handle.children().hide(); 5016 }; 5017 }; 5018 5019 var AutoLink = function (context) { 5020 var self = this; 5021 var defaultScheme = 'http://'; 5022 var linkPattern = /^([A-Za-z][A-Za-z0-9+-.]*\:[\/\/]?|mailto:[A-Z0-9._%+-]+@)?(www\.)?(.+)$/i; 5023 5024 this.events = { 5025 'summernote.keyup': function (we, e) { 5026 if (!e.isDefaultPrevented()) { 5027 self.handleKeyup(e); 5028 } 5029 }, 5030 'summernote.keydown': function (we, e) { 5031 self.handleKeydown(e); 5032 } 5033 }; 5034 5035 this.initialize = function () { 5036 this.lastWordRange = null; 5037 }; 5038 5039 this.destroy = function () { 5040 this.lastWordRange = null; 5041 }; 5042 5043 this.replace = function () { 5044 if (!this.lastWordRange) { 5045 return; 5046 } 5047 5048 var keyword = this.lastWordRange.toString(); 5049 var match = keyword.match(linkPattern); 5050 5051 if (match && (match[1] || match[2])) { 5052 var link = match[1] ? keyword : defaultScheme + keyword; 5053 var node = $('<a />').html(keyword).attr('href', link)[0]; 5054 5055 this.lastWordRange.insertNode(node); 5056 this.lastWordRange = null; 5057 context.invoke('editor.focus'); 5058 } 5059 5060 }; 5061 5062 this.handleKeydown = function (e) { 5063 if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) {
5064 var wordRange = context.invoke('editor.createRange').getWordRange(); 5065 this.lastWordRange = wordRange; 5066 } 5067 }; 5068 5069 this.handleKeyup = function (e) { 5070 if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) { 5071 this.replace(); 5072 } 5073 }; 5074 }; 5075 5076 /** 5077 * textarea auto sync. 5078 */ 5079 var AutoSync = function (context) { 5080 var $note = context.layoutInfo.note; 5081 5082 this.events = { 5083 'summernote.change': function () { 5084 $note.val(context.invoke('code')); 5085 } 5086 }; 5087 5088 this.shouldInitialize = function () { 5089 return dom.isTextarea($note[0]); 5090 }; 5091 }; 5092 5093 var Placeholder = function (context) { 5094 var self = this; 5095 var $editingArea = context.layoutInfo.editingArea; 5096 var options = context.options; 5097 5098 this.events = { 5099 'summernote.init summernote.change': function () { 5100 self.update(); 5101 }, 5102 'summernote.codeview.toggled': function () { 5103 self.update(); 5104 } 5105 }; 5106 5107 this.shouldInitialize = function () { 5108 return !!options.placeholder; 5109 }; 5110 5111 this.initialize = function () { 5112 this.$placeholder = $('<div class="note-placeholder">'); 5113 this.$placeholder.on('click', function () { 5114 context.invoke('focus'); 5115 }).text(options.placeholder).prependTo($editingArea); 5116 }; 5117 5118 this.destroy = function () { 5119 this.$placeholder.remove(); 5120 }; 5121 5122 this.update = function () { 5123 var isShow = !context.invoke('codeview.isActivated') && context.invoke('editor.isEmpty'); 5124 this.$placeholder.toggle(isShow); 5125 }; 5126 }; 5127 5128 var Buttons = function (context) { 5129 var self = this; 5130 var ui = $.summernote.ui; 5131 5132 var $toolbar = context.layoutInfo.toolbar; 5133 var options = context.options; 5134 var lang = options.langInfo; 5135 5136 var invertedKeyMap = func.invertObject(options.keyMap[agent.isMac ? 'mac' : 'pc']); 5137 5138 var representShortcut = this.representShortcut = function (editorMethod) { 5139 var shortcut = invertedKeyMap[editorMethod]; 5140 if (!options.shortcuts || !shortcut) { 5141 return ''; 5142 } 5143 5144 if (agent.isMac) { 5145 shortcut = shortcut.replace('CMD', '⌘').replace('SHIFT', '⇧'); 5146 } 5147 5148 shortcut = shortcut.replace('BACKSLASH', '\\') 5149 .replace('SLASH', '/') 5150 .replace('LEFTBRACKET', '[') 5151 .replace('RIGHTBRACKET', ']'); 5152 5153 return ' (' + shortcut + ')'; 5154 }; 5155 5156 this.initialize = function () { 5157 this.addToolbarButtons(); 5158 this.addImagePopoverButtons(); 5159 this.addLinkPopoverButtons(); 5160 this.fontInstalledMap = {}; 5161 }; 5162 5163 this.destroy = function () { 5164 delete this.fontInstalledMap; 5165 }; 5166 5167 this.isFontInstalled = function (name) { 5168 if (!self.fontInstalledMap.hasOwnProperty(name)) { 5169 self.fontInstalledMap[name] = agent.isFontInstalled(name) || 5170 list.contains(options.fontNamesIgnoreCheck, name); 5171 } 5172 5173 return self.fontInstalledMap[name]; 5174 }; 5175 5176 this.addToolbarButtons = function () { 5177 context.memo('button.style', function () { 5178 return ui.buttonGroup([ 5179 ui.button({ 5180 className: 'dropdown-toggle', 5181 contents: ui.icon(options.icons.magic) + ' ' + ui.icon(options.icons.caret, 'span'), 5182 tooltip: lang.style.style, 5183 data: { 5184 toggle: 'dropdown' 5185 } 5186 }), 5187 ui.dropdown({ 5188 className: 'dropdown-style', 5189 items: context.options.styleTags, 5190 template: function (item) { 5191 5192 if (typeof item === 'string') { 5193 item = { tag: item, title: (lang.style.hasOwnProperty(item) ? lang.style[item] : item) }; 5194 } 5195 5196 var tag = item.tag; 5197 var title = item.title; 5198 var style = item.style ? ' style="' + item.style + '" ' : ''; 5199 var className = item.className ? ' class="' + item.className + '"' : ''; 5200 5201 return '<' + tag + style + className + '>' + title + '</' + tag + '>'; 5202 }, 5203 click: context.createInvokeHandler('editor.formatBlock') 5204 }) 5205 ]).render(); 5206 }); 5207 5208 context.memo('button.bold', function () { 5209 return ui.button({ 5210 className: 'note-btn-bold', 5211 contents: ui.icon(options.icons.bold), 5212 tooltip: lang.font.bold + representShortcut('bold'),
5213 click: context.createInvokeHandler('editor.bold') 5214 }).render(); 5215 }); 5216 5217 context.memo('button.italic', function () { 5218 return ui.button({ 5219 className: 'note-btn-italic', 5220 contents: ui.icon(options.icons.italic), 5221 tooltip: lang.font.italic + representShortcut('italic'), 5222 click: context.createInvokeHandler('editor.italic') 5223 }).render(); 5224 }); 5225 5226 context.memo('button.underline', function () { 5227 return ui.button({ 5228 className: 'note-btn-underline', 5229 contents: ui.icon(options.icons.underline), 5230 tooltip: lang.font.underline + representShortcut('underline'), 5231 click: context.createInvokeHandler('editor.underline') 5232 }).render(); 5233 }); 5234 5235 context.memo('button.clear', function () { 5236 return ui.button({ 5237 contents: ui.icon(options.icons.eraser), 5238 tooltip: lang.font.clear + representShortcut('removeFormat'), 5239 click: context.createInvokeHandler('editor.removeFormat') 5240 }).render(); 5241 }); 5242 5243 context.memo('button.strikethrough', function () { 5244 return ui.button({ 5245 className: 'note-btn-strikethrough', 5246 contents: ui.icon(options.icons.strikethrough), 5247 tooltip: lang.font.strikethrough + representShortcut('strikethrough'), 5248 click: context.createInvokeHandler('editor.strikethrough') 5249 }).render(); 5250 }); 5251 5252 context.memo('button.superscript', function () { 5253 return ui.button({ 5254 className: 'note-btn-superscript', 5255 contents: ui.icon(options.icons.superscript), 5256 tooltip: lang.font.superscript, 5257 click: context.createInvokeHandler('editor.superscript') 5258 }).render(); 5259 }); 5260 5261 context.memo('button.subscript', function () { 5262 return ui.button({ 5263 className: 'note-btn-subscript', 5264 contents: ui.icon(options.icons.subscript), 5265 tooltip: lang.font.subscript, 5266 click: context.createInvokeHandler('editor.subscript') 5267 }).render(); 5268 }); 5269 5270 context.memo('button.fontname', function () { 5271 return ui.buttonGroup([ 5272 ui.button({ 5273 className: 'dropdown-toggle', 5274 contents: '<span class="note-current-fontname"/> ' + ui.icon(options.icons.caret, 'span'), 5275 tooltip: lang.font.name, 5276 data: { 5277 toggle: 'dropdown' 5278 } 5279 }), 5280 ui.dropdownCheck({ 5281 className: 'dropdown-fontname', 5282 checkClassName: options.icons.menuCheck, 5283 items: options.fontNames.filter(self.isFontInstalled), 5284 template: function (item) { 5285 return '<span style="font-family:' + item + '">' + item + '</span>'; 5286 }, 5287 click: context.createInvokeHandler('editor.fontName') 5288 }) 5289 ]).render(); 5290 }); 5291 5292 context.memo('button.fontsize', function () { 5293 return ui.buttonGroup([ 5294 ui.button({ 5295 className: 'dropdown-toggle', 5296 contents: '<span class="note-current-fontsize"/>' + ui.icon(options.icons.caret, 'span'), 5297 tooltip: lang.font.size, 5298 data: { 5299 toggle: 'dropdown' 5300 } 5301 }), 5302 ui.dropdownCheck({ 5303 className: 'dropdown-fontsize', 5304 checkClassName: options.icons.menuCheck, 5305 items: options.fontSizes, 5306 click: context.createInvokeHandler('editor.fontSize') 5307 }) 5308 ]).render(); 5309 }); 5310 5311 context.memo('button.color', function () { 5312 return ui.buttonGroup({ 5313 className: 'note-color', 5314 children: [ 5315 ui.button({ 5316 className: 'note-current-color-button', 5317 contents: ui.icon(options.icons.font + ' note-recent-color'), 5318 tooltip: lang.color.recent, 5319 click: function (e) { 5320 var $button = $(e.currentTarget); 5321 context.invoke('editor.color', { 5322 backColor: $button.attr('data-backColor'), 5323 foreColor: $button.attr('data-foreColor') 5324 }); 5325 }, 5326 callback: function ($button) { 5327 var $recentColor = $button.find('.note-recent-color'); 5328 $recentColor.css('background-color', '#FFFF00'); 5329 $button.attr('data-backColor', '#FFFF00'); 5330 } 5331 }), 5332 ui.button({ 5333 className: 'dropdown-toggle', 5334 contents: ui.icon(options.icons.caret, 'span'), 5335 tooltip: lang.color.more, 5336 data: { 5337 toggle: 'dropdown' 5338 } 5339 }), 5340 ui.dropdown({ 5341 items: [ 5342 '<li>', 5343 '<div class="btn-group">', 5344 ' <div class="note-palette-title">' + lang.color.background + '</div>', 5345 ' <div>', 5346 ' <button type="button" class="note-color-reset btn btn-default" data-event="backColor" data-value="inherit">', 5347 lang.color.transparent, 5348 ' </button>', 5349 ' </div>', 5350 ' <div class="note-holder" data-event="backColor"/>', 5351 '</div>', 5352 '<div class="btn-group">', 5353 ' <div class="note-palette-title">' + lang.color.foreground + '</div>', 5354 ' <div>', 5355 ' <button type="button" class="note-color-reset btn btn-default" data-event="removeFormat" data-value="foreColor">', 5356 lang.color.resetToDefault, 5357 ' </button>', 5358 ' </div>', 5359 ' <div class="note-holder" data-event="foreColor"/>', 5360 '</div>', 5361 '</li>' 5362 ].join(''), 5363 callback: function ($dropdown) { 5364 $dropdown.find('.note-holder').each(function () { 5365 var $holder = $(this); 5366 $holder.append(ui.palette({ 5367 colors: options.colors, 5368 eventName: $holder.data('event') 5369 }).render()); 5370 }); 5371 }, 5372 click: function (event) { 5373 var $button = $(event.target); 5374 var eventName = $button.data('event'); 5375 var value = $button.data('value'); 5376 5377 if (eventName && value) { 5378 var key = eventName === 'backColor' ? 'background-color' : 'color'; 5379 var $color = $button.closest('.note-color').find('.note-recent-color'); 5380 var $currentButton = $button.closest('.note-color').find('.note-current-color-button'); 5381 5382 $color.css(key, value); 5383 $currentButton.attr('data-' + eventName, value);
5384 context.invoke('editor.' + eventName, value); 5385 } 5386 } 5387 }) 5388 ] 5389 }).render(); 5390 }); 5391 5392 context.memo('button.ul', function () { 5393 return ui.button({ 5394 contents: ui.icon(options.icons.unorderedlist), 5395 tooltip: lang.lists.unordered + representShortcut('insertUnorderedList'), 5396 click: context.createInvokeHandler('editor.insertUnorderedList') 5397 }).render(); 5398 }); 5399 5400 context.memo('button.ol', function () { 5401 return ui.button({ 5402 contents: ui.icon(options.icons.orderedlist), 5403 tooltip: lang.lists.ordered + representShortcut('insertOrderedList'), 5404 click: context.createInvokeHandler('editor.insertOrderedList') 5405 }).render(); 5406 }); 5407 5408 var justifyLeft = ui.button({ 5409 contents: ui.icon(options.icons.alignLeft), 5410 tooltip: lang.paragraph.left + representShortcut('justifyLeft'), 5411 click: context.createInvokeHandler('editor.justifyLeft') 5412 }); 5413 5414 var justifyCenter = ui.button({ 5415 contents: ui.icon(options.icons.alignCenter), 5416 tooltip: lang.paragraph.center + representShortcut('justifyCenter'), 5417 click: context.createInvokeHandler('editor.justifyCenter') 5418 }); 5419 5420 var justifyRight = ui.button({ 5421 contents: ui.icon(options.icons.alignRight), 5422 tooltip: lang.paragraph.right + representShortcut('justifyRight'), 5423 click: context.createInvokeHandler('editor.justifyRight') 5424 }); 5425 5426 var justifyFull = ui.button({ 5427 contents: ui.icon(options.icons.alignJustify), 5428 tooltip: lang.paragraph.justify + representShortcut('justifyFull'), 5429 click: context.createInvokeHandler('editor.justifyFull') 5430 }); 5431 5432 var outdent = ui.button({ 5433 contents: ui.icon(options.icons.outdent), 5434 tooltip: lang.paragraph.outdent + representShortcut('outdent'), 5435 click: context.createInvokeHandler('editor.outdent') 5436 }); 5437 5438 var indent = ui.button({ 5439 contents: ui.icon(options.icons.indent), 5440 tooltip: lang.paragraph.indent + representShortcut('indent'), 5441 click: context.createInvokeHandler('editor.indent') 5442 }); 5443 5444 context.memo('button.justifyLeft', func.invoke(justifyLeft, 'render')); 5445 context.memo('button.justifyCenter', func.invoke(justifyCenter, 'render')); 5446 context.memo('button.justifyRight', func.invoke(justifyRight, 'render')); 5447 context.memo('button.justifyFull', func.invoke(justifyFull, 'render')); 5448 context.memo('button.outdent', func.invoke(outdent, 'render')); 5449 context.memo('button.indent', func.invoke(indent, 'render')); 5450 5451 context.memo('button.paragraph', function () { 5452 return ui.buttonGroup([ 5453 ui.button({ 5454 className: 'dropdown-toggle', 5455 contents: ui.icon(options.icons.alignLeft) + ' ' + ui.icon(options.icons.caret, 'span'), 5456 tooltip: lang.paragraph.paragraph, 5457 data: { 5458 toggle: 'dropdown' 5459 } 5460 }), 5461 ui.dropdown([ 5462 ui.buttonGroup({ 5463 className: 'note-align', 5464 children: [justifyLeft, justifyCenter, justifyRight, justifyFull] 5465 }), 5466 ui.buttonGroup({ 5467 className: 'note-list', 5468 children: [outdent, indent] 5469 }) 5470 ]) 5471 ]).render(); 5472 }); 5473 5474 context.memo('button.height', function () { 5475 return ui.buttonGroup([ 5476 ui.button({ 5477 className: 'dropdown-toggle', 5478 contents: ui.icon(options.icons.textHeight) + ' ' + ui.icon(options.icons.caret, 'span'), 5479 tooltip: lang.font.height, 5480 data: { 5481 toggle: 'dropdown' 5482 } 5483 }), 5484 ui.dropdownCheck({ 5485 items: options.lineHeights, 5486 checkClassName: options.icons.menuCheck, 5487 className: 'dropdown-line-height', 5488 click: context.createInvokeHandler('editor.lineHeight') 5489 }) 5490 ]).render(); 5491 }); 5492 5493 context.memo('button.table', function () { 5494 return ui.buttonGroup([ 5495 ui.button({ 5496 className: 'dropdown-toggle', 5497 contents: ui.icon(options.icons.table) + ' ' + ui.icon(options.icons.caret, 'span'), 5498 tooltip: lang.table.table, 5499 data: { 5500 toggle: 'dropdown' 5501 } 5502 }), 5503 ui.dropdown({ 5504 className: 'note-table', 5505 items: [ 5506 '<div class="note-dimension-picker">', 5507 ' <div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"/>', 5508 ' <div class="note-dimension-picker-highlighted"/>', 5509 ' <div class="note-dimension-picker-unhighlighted"/>', 5510 '</div>', 5511 '<div class="note-dimension-display">1 x 1</div>' 5512 ].join('') 5513 }) 5514 ], { 5515 callback: function ($node) { 5516 var $catcher = $node.find('.note-dimension-picker-mousecatcher'); 5517 $catcher.css({ 5518 width: options.insertTableMaxSize.col + 'em', 5519 height: options.insertTableMaxSize.row + 'em' 5520 }).mousedown(context.createInvokeHandler('editor.insertTable')) 5521 .on('mousemove', self.tableMoveHandler); 5522 } 5523 }).render(); 5524 }); 5525 5526 context.memo('button.link', function () { 5527 return ui.button({ 5528 contents: ui.icon(options.icons.link), 5529 tooltip: lang.link.link + representShortcut('linkDialog.show'), 5530 click: context.createInvokeHandler('linkDialog.show') 5531 }).render(); 5532 }); 5533 5534 context.memo('button.hr', function () { 5535 return ui.button({ 5536 contents: ui.icon(options.icons.minus), 5537 tooltip: lang.hr.insert + representShortcut('insertHorizontalRule'), 5538 click: context.createInvokeHandler('editor.insertHorizontalRule') 5539 }).render(); 5540 }); 5541 5542 context.memo('button.fullscreen', function () { 5543 return ui.button({ 5544 className: 'btn-fullscreen', 5545 contents: ui.icon(options.icons.arrowsAlt), 5546 tooltip: lang.options.fullscreen, 5547 click: context.createInvokeHandler('fullscreen.toggle') 5548 }).render(); 5549 }); 5550 5551 /* context.memo('button.codeview', function () { 5552 return ui.button({ 5553 className: 'btn-codeview', 5554 contents: ui.icon(options.icons.code), 5555 tooltip: lang.options.codeview, 5556 click: context.createInvokeHandler('codeview.toggle') 5557 }).render(); 5558 }); */ 5559 5560 context.memo('button.redo', function () { 5561 return ui.button({ 5562 contents: ui.icon(options.icons.redo), 5563 tooltip: lang.history.redo + representShortcut('redo'), 5564 click: context.createInvokeHandler('editor.redo') 5565 }).render(); 5566 }); 5567 5568 context.memo('button.undo', function () { 5569 return ui.button({ 5570 contents: ui.icon(options.icons.undo), 5571 tooltip: lang.history.undo + representShortcut('undo'), 5572 click: context.createInvokeHandler('editor.undo') 5573 }).render(); 5574 }); 5575 5576 context.memo('button.help', function () { 5577 return ui.button({ 5578 contents: ui.icon(options.icons.question), 5579 tooltip: lang.options.help, 5580 click: context.createInvokeHandler('helpDialog.show') 5581 }).render(); 5582 }); 5583 }; 5584 5585 /** 5586 * image : [ 5587 * ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']], 5588 * ['float', ['floatLeft', 'floatRight', 'floatNone' ]], 5589 * ['remove', ['removeMedia']] 5590 * ], 5591 */ 5592 this.addImagePopoverButtons = function () { 5593 // Image Size Buttons 5594 context.memo('button.imageSize100', function () { 5595 return ui.button({ 5596 contents: '<span class="note-fontsize-10">100%</span>',
5597 tooltip: lang.image.resizeFull, 5598 click: context.createInvokeHandler('editor.resize', '1') 5599 }).render(); 5600 }); 5601 context.memo('button.imageSize50', function () { 5602 return ui.button({ 5603 contents: '<span class="note-fontsize-10">50%</span>', 5604 tooltip: lang.image.resizeHalf, 5605 click: context.createInvokeHandler('editor.resize', '0.5') 5606 }).render(); 5607 }); 5608 context.memo('button.imageSize25', function () { 5609 return ui.button({ 5610 contents: '<span class="note-fontsize-10">25%</span>', 5611 tooltip: lang.image.resizeQuarter, 5612 click: context.createInvokeHandler('editor.resize', '0.25') 5613 }).render(); 5614 }); 5615 5616 // Float Buttons 5617 context.memo('button.floatLeft', function () { 5618 return ui.button({ 5619 contents: ui.icon(options.icons.alignLeft), 5620 tooltip: lang.image.floatLeft, 5621 click: context.createInvokeHandler('editor.floatMe', 'left') 5622 }).render(); 5623 }); 5624 5625 context.memo('button.floatRight', function () { 5626 return ui.button({ 5627 contents: ui.icon(options.icons.alignRight), 5628 tooltip: lang.image.floatRight, 5629 click: context.createInvokeHandler('editor.floatMe', 'right') 5630 }).render(); 5631 }); 5632 5633 context.memo('button.floatNone', function () { 5634 return ui.button({ 5635 contents: ui.icon(options.icons.alignJustify), 5636 tooltip: lang.image.floatNone, 5637 click: context.createInvokeHandler('editor.floatMe', 'none') 5638 }).render(); 5639 }); 5640 5641 // Remove Buttons 5642 context.memo('button.removeMedia', function () { 5643 return ui.button({ 5644 contents: ui.icon(options.icons.trash), 5645 tooltip: lang.image.remove, 5646 click: context.createInvokeHandler('editor.removeMedia') 5647 }).render(); 5648 }); 5649 }; 5650 5651 this.addLinkPopoverButtons = function () { 5652 context.memo('button.linkDialogShow', function () { 5653 return ui.button({ 5654 contents: ui.icon(options.icons.link), 5655 tooltip: lang.link.edit, 5656 click: context.createInvokeHandler('linkDialog.show') 5657 }).render(); 5658 }); 5659 5660 context.memo('button.unlink', function () { 5661 return ui.button({ 5662 contents: ui.icon(options.icons.unlink), 5663 tooltip: lang.link.unlink, 5664 click: context.createInvokeHandler('editor.unlink') 5665 }).render(); 5666 }); 5667 }; 5668 5669 this.build = function ($container, groups) { 5670 for (var groupIdx = 0, groupLen = groups.length; groupIdx < groupLen; groupIdx++) { 5671 var group = groups[groupIdx]; 5672 var groupName = group[0]; 5673 var buttons = group[1]; 5674 5675 var $group = ui.buttonGroup({ 5676 className: 'note-' + groupName 5677 }).render(); 5678 5679 for (var idx = 0, len = buttons.length; idx < len; idx++) { 5680 var button = context.memo('button.' + buttons[idx]); 5681 if (button) { 5682 $group.append(typeof button === 'function' ? button(context) : button); 5683 } 5684 } 5685 $group.appendTo($container); 5686 } 5687 }; 5688 5689 this.updateCurrentStyle = function () { 5690 var styleInfo = context.invoke('editor.currentStyle'); 5691 this.updateBtnStates({ 5692 '.note-btn-bold': function () { 5693 return styleInfo['font-bold'] === 'bold'; 5694 }, 5695 '.note-btn-italic': function () { 5696 return styleInfo['font-italic'] === 'italic'; 5697 }, 5698 '.note-btn-underline': function () { 5699 return styleInfo['font-underline'] === 'underline'; 5700 }, 5701 '.note-btn-subscript': function () { 5702 return styleInfo['font-subscript'] === 'subscript'; 5703 }, 5704 '.note-btn-superscript': function () { 5705 return styleInfo['font-superscript'] === 'superscript'; 5706 }, 5707 '.note-btn-strikethrough': function () { 5708 return styleInfo['font-strikethrough'] === 'strikethrough'; 5709 } 5710 }); 5711 5712 if (styleInfo['font-family']) { 5713 var fontNames = styleInfo['font-family'].split(',').map(function (name) { 5714 return name.replace(/[\'\"]/g, '') 5715 .replace(/\s+$/, '') 5716 .replace(/^\s+/, ''); 5717 }); 5718 var fontName = list.find(fontNames, self.isFontInstalled); 5719 5720 $toolbar.find('.dropdown-fontname li a').each(function () {
5721 // always compare string to avoid creating another func. 5722 var isChecked = ($(this).data('value') + '') === (fontName + ''); 5723 this.className = isChecked ? 'checked' : ''; 5724 }); 5725 $toolbar.find('.note-current-fontname').text(fontName); 5726 } 5727 5728 if (styleInfo['font-size']) { 5729 var fontSize = styleInfo['font-size']; 5730 $toolbar.find('.dropdown-fontsize li a').each(function () { 5731 // always compare with string to avoid creating another func. 5732 var isChecked = ($(this).data('value') + '') === (fontSize + ''); 5733 this.className = isChecked ? 'checked' : ''; 5734 }); 5735 $toolbar.find('.note-current-fontsize').text(fontSize); 5736 } 5737 5738 if (styleInfo['line-height']) { 5739 var lineHeight = styleInfo['line-height']; 5740 $toolbar.find('.dropdown-line-height li a').each(function () { 5741 // always compare with string to avoid creating another func. 5742 var isChecked = ($(this).data('value') + '') === (lineHeight + ''); 5743 this.className = isChecked ? 'checked' : ''; 5744 }); 5745 } 5746 }; 5747 5748 this.updateBtnStates = function (infos) { 5749 $.each(infos, function (selector, pred) { 5750 ui.toggleBtnActive($toolbar.find(selector), pred()); 5751 }); 5752 }; 5753 5754 this.tableMoveHandler = function (event) { 5755 var PX_PER_EM = 18; 5756 var $picker = $(event.target.parentNode); // target is mousecatcher 5757 var $dimensionDisplay = $picker.next(); 5758 var $catcher = $picker.find('.note-dimension-picker-mousecatcher'); 5759 var $highlighted = $picker.find('.note-dimension-picker-highlighted'); 5760 var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted'); 5761 5762 var posOffset; 5763 // HTML5 with jQuery - e.offsetX is undefined in Firefox 5764 if (event.offsetX === undefined) { 5765 var posCatcher = $(event.target).offset(); 5766 posOffset = { 5767 x: event.pageX - posCatcher.left, 5768 y: event.pageY - posCatcher.top 5769 }; 5770 } else { 5771 posOffset = { 5772 x: event.offsetX, 5773 y: event.offsetY 5774 }; 5775 } 5776 5777 var dim = { 5778 c: Math.ceil(posOffset.x / PX_PER_EM) || 1, 5779 r: Math.ceil(posOffset.y / PX_PER_EM) || 1 5780 }; 5781 5782 $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' }); 5783 $catcher.data('value', dim.c + 'x' + dim.r); 5784 5785 if (3 < dim.c && dim.c < options.insertTableMaxSize.col) { 5786 $unhighlighted.css({ width: dim.c + 1 + 'em'}); 5787 } 5788 5789 if (3 < dim.r && dim.r < options.insertTableMaxSize.row) { 5790 $unhighlighted.css({ height: dim.r + 1 + 'em'}); 5791 } 5792 5793 $dimensionDisplay.html(dim.c + ' x ' + dim.r); 5794 }; 5795 }; 5796 5797 var Toolbar = function (context) { 5798 var ui = $.summernote.ui; 5799 5800 var $note = context.layoutInfo.note; 5801 var $toolbar = context.layoutInfo.toolbar; 5802 var options = context.options; 5803 5804 this.shouldInitialize = function () { 5805 return !options.airMode; 5806 }; 5807 5808 this.initialize = function () { 5809 options.toolbar = options.toolbar || []; 5810 5811 if (!options.toolbar.length) { 5812 $toolbar.hide(); 5813 } else { 5814 context.invoke('buttons.build', $toolbar, options.toolbar); 5815 } 5816 5817 if (options.toolbarContainer) { 5818 $toolbar.appendTo(options.toolbarContainer); 5819 } 5820 5821 $note.on('summernote.keyup summernote.mouseup summernote.change', function () { 5822 context.invoke('buttons.updateCurrentStyle'); 5823 }); 5824 5825 context.invoke('buttons.updateCurrentStyle'); 5826 }; 5827 5828 this.destroy = function () { 5829 $toolbar.children().remove(); 5830 }; 5831 5832 this.updateFullscreen = function (isFullscreen) { 5833 ui.toggleBtnActive($toolbar.find('.btn-fullscreen'), isFullscreen); 5834 }; 5835 5836 /* this.updateCodeview = function (isCodeview) { 5837 ui.toggleBtnActive($toolbar.find('.btn-codeview'), isCodeview); 5838 if (isCodeview) { 5839 this.deactivate(); 5840 } else { 5841 this.activate(); 5842 } 5843 }; */ 5844 5845 /* this.activate = function (isIncludeCodeview) { 5846 var $btn = $toolbar.find('button'); 5847 if (!isIncludeCodeview) { 5848 $btn = $btn.not('.btn-codeview'); 5849 } 5850 ui.toggleBtn($btn, true); 5851 }; */ 5852 5853 /* this.deactivate = function (isIncludeCodeview) { 5854 var $btn = $toolbar.find('button'); 5855 if (!isIncludeCodeview) { 5856 $btn = $btn.not('.btn-codeview'); 5857 } 5858 ui.toggleBtn($btn, false); 5859 }; */ 5860 }; 5861 5862 var LinkDialog = function (context) { 5863 var self = this; 5864 var ui = $.summernote.ui; 5865 5866 var $editor = context.layoutInfo.editor; 5867 var options = context.options; 5868 var lang = options.langInfo; 5869 5870 this.initialize = function () { 5871 var $container = options.dialogsInBody ? $(document.body) : $editor; 5872 5873 var body = '<div class="form-group">' + 5874 '<label>' + lang.link.textToDisplay + '</label>' + 5875 '<input class="note-link-text form-control" type="text" />' + 5876 '</div>' + 5877 '<div class="form-group">' + 5878 '<label>' + lang.link.url + '</label>' + 5879 '<input class="note-link-url form-control" type="text" value="http://" />' + 5880 '</div>' + 5881 (!options.disableLinkTarget ? 5882 '<div class="checkbox">' + 5883 '<label>' + '<input type="checkbox" checked> ' + lang.link.openInNewWindow + '</label>' + 5884 '</div>' : '' 5885 ); 5886 var footer = '<button href="#" class="btn btn-primary note-link-btn disabled" disabled>' + lang.link.insert + '</button>'; 5887 5888 this.$dialog = ui.dialog({ 5889 className: 'link-dialog', 5890 title: lang.link.insert, 5891 fade: options.dialogsFade, 5892 body: body, 5893 footer: footer 5894 }).render().appendTo($container); 5895 }; 5896 5897 this.destroy = function () { 5898 ui.hideDialog(this.$dialog); 5899 this.$dialog.remove(); 5900 }; 5901 5902 this.bindEnterKey = function ($input, $btn) { 5903 $input.on('keypress', function (event) { 5904 if (event.keyCode === key.code.ENTER) { 5905 $btn.trigger('click'); 5906 } 5907 }); 5908 }; 5909 5910 /** 5911 * toggle update button 5912 */ 5913 this.toggleLinkBtn = function ($linkBtn, $linkText, $linkUrl) { 5914 ui.toggleBtn($linkBtn, $linkText.val() && $linkUrl.val()); 5915 }; 5916 5917 /** 5918 * Show link dialog and set event handlers on dialog controls. 5919 * 5920 * @param {Object} linkInfo 5921 * @return {Promise} 5922 */ 5923 this.showLinkDialog = function (linkInfo) { 5924 return $.Deferred(function (deferred) { 5925 var $linkText = self.$dialog.find('.note-link-text'), 5926 $linkUrl = self.$dialog.find('.note-link-url'), 5927 $linkBtn = self.$dialog.find('.note-link-btn'), 5928 $openInNewWindow = self.$dialog.find('input[type=checkbox]'); 5929 5930 ui.onDialogShown(self.$dialog, function () { 5931 context.triggerEvent('dialog.shown'); 5932 5933 // if no url was given, copy text to url 5934 if (!linkInfo.url) { 5935 linkInfo.url = linkInfo.text; 5936 } 5937 5938 $linkText.val(linkInfo.text); 5939 5940 var handleLinkTextUpdate = function () { 5941 self.toggleLinkBtn($linkBtn, $linkText, $linkUrl); 5942 // if linktext was modified by keyup,
5943 // stop cloning text from linkUrl 5944 linkInfo.text = $linkText.val(); 5945 }; 5946 5947 $linkText.on('input', handleLinkTextUpdate).on('paste', function () { 5948 setTimeout(handleLinkTextUpdate, 0); 5949 }); 5950 5951 var handleLinkUrlUpdate = function () { 5952 self.toggleLinkBtn($linkBtn, $linkText, $linkUrl); 5953 // display same link on `Text to display` input 5954 // when create a new link 5955 if (!linkInfo.text) { 5956 $linkText.val($linkUrl.val()); 5957 } 5958 }; 5959 5960 $linkUrl.on('input', handleLinkUrlUpdate).on('paste', function () { 5961 setTimeout(handleLinkUrlUpdate, 0); 5962 }).val(linkInfo.url).trigger('focus'); 5963 5964 self.toggleLinkBtn($linkBtn, $linkText, $linkUrl); 5965 self.bindEnterKey($linkUrl, $linkBtn); 5966 self.bindEnterKey($linkText, $linkBtn); 5967 5968 $openInNewWindow.prop('checked', linkInfo.isNewWindow); 5969 5970 $linkBtn.one('click', function (event) { 5971 event.preventDefault(); 5972 5973 deferred.resolve({ 5974 range: linkInfo.range, 5975 url: $linkUrl.val(), 5976 text: $linkText.val(), 5977 isNewWindow: $openInNewWindow.is(':checked') 5978 }); 5979 self.$dialog.modal('hide'); 5980 }); 5981 }); 5982 5983 ui.onDialogHidden(self.$dialog, function () { 5984 // detach events 5985 $linkText.off('input paste keypress'); 5986 $linkUrl.off('input paste keypress'); 5987 $linkBtn.off('click'); 5988 5989 if (deferred.state() === 'pending') { 5990 deferred.reject(); 5991 } 5992 }); 5993 5994 ui.showDialog(self.$dialog); 5995 }).promise(); 5996 }; 5997 5998 /** 5999 * @param {Object} layoutInfo 6000 */ 6001 this.show = function () { 6002 var linkInfo = context.invoke('editor.getLinkInfo'); 6003 6004 context.invoke('editor.saveRange'); 6005 this.showLinkDialog(linkInfo).then(function (linkInfo) { 6006 context.invoke('editor.restoreRange'); 6007 context.invoke('editor.createLink', linkInfo); 6008 }).fail(function () { 6009 context.invoke('editor.restoreRange'); 6010 }); 6011 }; 6012 context.memo('help.linkDialog.show', options.langInfo.help['linkDialog.show']); 6013 }; 6014 6015 var LinkPopover = function (context) { 6016 var self = this; 6017 var ui = $.summernote.ui; 6018 6019 var options = context.options; 6020 6021 this.events = { 6022 'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function () { 6023 self.update(); 6024 }, 6025 'summernote.dialog.shown': function () { 6026 self.hide(); 6027 } 6028 }; 6029 6030 this.shouldInitialize = function () { 6031 return !list.isEmpty(options.popover.link); 6032 }; 6033 6034 this.initialize = function () { 6035 this.$popover = ui.popover({ 6036 className: 'note-link-popover', 6037 callback: function ($node) { 6038 var $content = $node.find('.popover-content'); 6039 $content.prepend('<span><a target="_blank"></a> </span>'); 6040 } 6041 }).render().appendTo('body'); 6042 var $content = this.$popover.find('.popover-content'); 6043 6044 context.invoke('buttons.build', $content, options.popover.link); 6045 }; 6046 6047 this.destroy = function () { 6048 this.$popover.remove(); 6049 }; 6050 6051 this.update = function () { 6052 // Prevent focusing on editable when invoke('code') is executed 6053 if (!context.invoke('editor.hasFocus')) { 6054 this.hide(); 6055 return; 6056 } 6057
6058 var rng = context.invoke('editor.createRange'); 6059 if (rng.isCollapsed() && rng.isOnAnchor()) { 6060 var anchor = dom.ancestor(rng.sc, dom.isAnchor); 6061 var href = $(anchor).attr('href'); 6062 this.$popover.find('a').attr('href', href).html(href); 6063 6064 var pos = dom.posFromPlaceholder(anchor); 6065 this.$popover.css({ 6066 display: 'block', 6067 left: pos.left, 6068 top: pos.top 6069 }); 6070 } else { 6071 this.hide(); 6072 } 6073 }; 6074 6075 this.hide = function () { 6076 this.$popover.hide(); 6077 }; 6078 }; 6079 6080 var ImageDialog = function (context) { 6081 var self = this; 6082 var ui = $.summernote.ui; 6083 6084 var $editor = context.layoutInfo.editor; 6085 var options = context.options; 6086 var lang = options.langInfo; 6087 6088 this.initialize = function () { 6089 var $container = options.dialogsInBody ? $(document.body) : $editor; 6090 6091 var imageLimitation = ''; 6092 if (options.maximumImageFileSize) { 6093 var unit = Math.floor(Math.log(options.maximumImageFileSize) / Math.log(1024)); 6094 var readableSize = (options.maximumImageFileSize / Math.pow(1024, unit)).toFixed(2) * 1 + 6095 ' ' + ' KMGTP'[unit] + 'B'; 6096 imageLimitation = '<small>' + lang.image.maximumFileSize + ' : ' + readableSize + '</small>'; 6097 } 6098 6099 var body = '<div class="form-group note-group-select-from-files">' + 6100 '<label>' + lang.image.selectFromFiles + '</label>' + 6101 '<input class="note-image-input form-control" type="file" name="files" accept="image/*" multiple="multiple" />' + 6102 imageLimitation + 6103 '</div>' + 6104 '<div class="form-group note-group-image-url" style="overflow:auto;">' + 6105 '<label>' + lang.image.url + '</label>' + 6106 '<input class="note-image-url form-control col-md-12" type="text" />' + 6107 '</div>'; 6108 var footer = '<button href="#" class="btn btn-primary note-image-btn disabled" disabled>' + lang.image.insert + '</button>'; 6109 6110 this.$dialog = ui.dialog({ 6111 title: lang.image.insert, 6112 fade: options.dialogsFade, 6113 body: body, 6114 footer: footer 6115 }).render().appendTo($container); 6116 }; 6117 6118 this.destroy = function () { 6119 ui.hideDialog(this.$dialog); 6120 this.$dialog.remove(); 6121 }; 6122 6123 this.bindEnterKey = function ($input, $btn) { 6124 $input.on('keypress', function (event) { 6125 if (event.keyCode === key.code.ENTER) { 6126 $btn.trigger('click'); 6127 } 6128 }); 6129 }; 6130 6131 this.show = function () { 6132 context.invoke('editor.saveRange'); 6133 this.showImageDialog().then(function (data) { 6134 // [workaround] hide dialog before restore range for IE range focus 6135 ui.hideDialog(self.$dialog); 6136 context.invoke('editor.restoreRange'); 6137 6138 if (typeof data === 'string') { // image url 6139 context.invoke('editor.insertImage', data); 6140 } else { // array of files 6141 context.invoke('editor.insertImagesOrCallback', data); 6142 } 6143 }).fail(function () { 6144 context.invoke('editor.restoreRange'); 6145 }); 6146 }; 6147 6148 /** 6149 * show image dialog 6150 * 6151 * @param {jQuery} $dialog 6152 * @return {Promise} 6153 */ 6154 this.showImageDialog = function () { 6155 return $.Deferred(function (deferred) { 6156 var $imageInput = self.$dialog.find('.note-image-input'), 6157 $imageUrl = self.$dialog.find('.note-image-url'), 6158 $imageBtn = self.$dialog.find('.note-image-btn'); 6159 6160 ui.onDialogShown(self.$dialog, function () { 6161 context.triggerEvent('dialog.shown'); 6162 6163 // Cloning imageInput to clear element. 6164 $imageInput.replaceWith($imageInput.clone() 6165 .on('change', function () { 6166 deferred.resolve(this.files || this.value); 6167 }) 6168 .val('') 6169 ); 6170 6171 $imageBtn.click(function (event) { 6172 event.preventDefault(); 6173 6174 deferred.resolve($imageUrl.val()); 6175 }); 6176 6177 $imageUrl.on('keyup paste', function () { 6178 var url = $imageUrl.val(); 6179 ui.toggleBtn($imageBtn, url); 6180 }).val('').trigger('focus'); 6181 self.bindEnterKey($imageUrl, $imageBtn); 6182 }); 6183 6184 ui.onDialogHidden(self.$dialog, function () { 6185 $imageInput.off('change'); 6186 $imageUrl.off('keyup paste keypress'); 6187 $imageBtn.off('click'); 6188 6189 if (deferred.state() === 'pending') { 6190 deferred.reject(); 6191 } 6192 }); 6193 6194 ui.showDialog(self.$dialog); 6195 }); 6196 }; 6197 }; 6198 6199 var ImagePopover = function (context) { 6200 var ui = $.summernote.ui; 6201 6202 var options = context.options; 6203 6204 this.shouldInitialize = function () { 6205 return !list.isEmpty(options.popover.image); 6206 }; 6207 6208 this.initialize = function () { 6209 this.$popover = ui.popover({ 6210 className: 'note-image-popover' 6211 }).render().appendTo('body'); 6212 var $content = this.$popover.find('.popover-content'); 6213
6214 context.invoke('buttons.build', $content, options.popover.image); 6215 }; 6216 6217 this.destroy = function () { 6218 this.$popover.remove(); 6219 }; 6220 6221 this.update = function (target) { 6222 if (dom.isImg(target)) { 6223 var pos = dom.posFromPlaceholder(target); 6224 this.$popover.css({ 6225 display: 'block', 6226 left: pos.left, 6227 top: pos.top 6228 }); 6229 } else { 6230 this.hide(); 6231 } 6232 }; 6233 6234 this.hide = function () { 6235 this.$popover.hide(); 6236 }; 6237 }; 6238 6239 var VideoDialog = function (context) { 6240 var self = this; 6241 var ui = $.summernote.ui; 6242 6243 var $editor = context.layoutInfo.editor; 6244 var options = context.options; 6245 var lang = options.langInfo; 6246 6247 this.initialize = function () { 6248 var $container = options.dialogsInBody ? $(document.body) : $editor; 6249 6250 var body = '<div class="form-group row-fluid">' + 6251 '<label>' + lang.video.url + ' <small class="text-muted">' + lang.video.providers + '</small></label>' + 6252 '<input class="note-video-url form-control span12" t
6252ype="text" />' + 6253 '</div>'; 6254 var footer = '<button href="#" class="btn btn-primary note-video-btn disabled" disabled>' + lang.video.insert + '</button>'; 6255 6256 this.$dialog = ui.dialog({ 6257 title: lang.video.insert, 6258 fade: options.dialogsFade, 6259 body: body, 6260 footer: footer 6261 }).render().appendTo($container); 6262 }; 6263 6264 this.destroy = function () { 6265 ui.hideDialog(this.$dialog); 6266 this.$dialog.remove(); 6267 }; 6268 6269 this.bindEnterKey = function ($input, $btn) { 6270 $input.on('keypress', function (event) { 6271 if (event.keyCode === key.code.ENTER) { 6272 $btn.trigger('click'); 6273 } 6274 }); 6275 }; 6276 6277 this.createVideoNode = function (url) { 6278 // video url patterns(youtube, instagram, vimeo, dailymotion, youku, mp4, ogg, webm) 6279 var ytRegExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; 6280 var ytMatch = url.match(ytRegExp); 6281 6282 var igRegExp = /(?:www\.|\/\/)instagram\.com\/p\/(.[a-zA-Z0-9_-]*)/; 6283 var igMatch = url.match(igRegExp); 6284 6285 var vRegExp = /\/\/vine\.co\/v\/([a-zA-Z0-9]+)/; 6286 var vMatch = url.match(vRegExp); 6287 6288 var vimRegExp = /\/\/(player\.)?vimeo\.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/; 6289 var vimMatch = url.match(vimRegExp); 6290 6291 var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/; 6292 var dmMatch = url.match(dmRegExp); 6293 6294 var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/; 6295 var youkuMatch = url.match(youkuRegExp); 6296 6297 var mp4RegExp = /^.+.(mp4|m4v)$/; 6298 var mp4Match = url.match(mp4RegExp); 6299 6300 var oggRegExp = /^.+.(ogg|ogv)$/; 6301 var oggMatch = url.match(oggRegExp); 6302 6303 var webmRegExp = /^.+.(webm)$/; 6304 var webmMatch = url.match(webmRegExp); 6305 6306 var $video; 6307 if (ytMatch && ytMatch[1].length === 11) { 6308 var youtubeId = ytMatch[1]; 6309 $video = $('<iframe>') 6310 .attr('frameborder', 0) 6311 .attr('src', '//www.youtube.com/embed/' + youtubeId) 6312 .attr('width', '640').attr('height', '360'); 6313 } else if (igMatch && igMatch[0].length) { 6314 $video = $('<iframe>') 6315 .attr('frameborder', 0) 6316 .attr('src', 'https://instagram.com/p/' + igMatch[1] + '/embed/') 6317 .attr('width', '612').attr('height', '710') 6318 .attr('scrolling', 'no') 6319 .attr('allowtransparency', 'true'); 6320 } else if (vMatch && vMatch[0].length) { 6321 $video = $('<iframe>') 6322 .attr('frameborder', 0) 6323 .attr('src', vMatch[0] + '/embed/simple') 6324 .attr('width', '600').attr('height', '600') 6325 .attr('class', 'vine-embed'); 6326 } else if (vimMatch && vimMatch[3].length) { 6327 $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') 6328 .attr('frameborder', 0) 6329 .attr('src', '//player.vimeo.com/video/' + vimMatch[3]) 6330 .attr('width', '640').attr('height', '360'); 6331 } else if (dmMatch && dmMatch[2].length) { 6332 $video = $('<iframe>') 6333 .attr('frameborder', 0) 6334 .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2]) 6335 .attr('width', '640').attr('height', '360'); 6336 } else if (youkuMatch && youkuMatch[1].length) { 6337 $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') 6338 .attr('frameborder', 0) 6339 .attr('height', '498') 6340 .attr('width', '510') 6341 .attr('src', '//player.youku.com/embed/' + youkuMatch[1]); 6342 } else if (mp4Match || oggMatch || webmMatch) { 6343 $video = $('<video controls>') 6344 .attr('src', url) 6345 .attr('width', '640').attr('height', '360'); 6346 } else { 6347 // this is not a known video link. Now what, Cat? Now what? 6348 return false; 6349 } 6350 6351 $video.addClass('note-video-clip'); 6352 6353 return $video[0]; 6354 }; 6355 6356 this.show = function () {
6357 var text = context.invoke('editor.getSelectedText'); 6358 context.invoke('editor.saveRange'); 6359 this.showVideoDialog(text).then(function (url) { 6360 // [workaround] hide dialog before restore range for IE range focus 6361 ui.hideDialog(self.$dialog); 6362 context.invoke('editor.restoreRange'); 6363 6364 // build node 6365 var $node = self.createVideoNode(url); 6366 6367 if ($node) { 6368 // insert video node 6369 context.invoke('editor.insertNode', $node); 6370 } 6371 }).fail(function () { 6372 context.invoke('editor.restoreRange'); 6373 }); 6374 }; 6375 6376 /** 6377 * show image dialog 6378 * 6379 * @param {jQuery} $dialog 6380 * @return {Promise} 6381 */ 6382 this.showVideoDialog = function (text) { 6383 return $.Deferred(function (deferred) { 6384 var $videoUrl = self.$dialog.find('.note-video-url'), 6385 $videoBtn = self.$dialog.find('.note-video-btn'); 6386 6387 ui.onDialogShown(self.$dialog, function () { 6388 context.triggerEvent('dialog.shown'); 6389 6390 $videoUrl.val(text).on('input', function () { 6391 ui.toggleBtn($videoBtn, $videoUrl.val()); 6392 }).trigger('focus'); 6393 6394 $videoBtn.click(function (event) { 6395 event.preventDefault(); 6396 6397 deferred.resolve($videoUrl.val()); 6398 }); 6399 6400 self.bindEnterKey($videoUrl, $videoBtn); 6401 }); 6402 6403 ui.onDialogHidden(self.$dialog, function () { 6404 $videoUrl.off('input'); 6405 $videoBtn.off('click'); 6406 6407 if (deferred.state() === 'pending') { 6408 deferred.reject(); 6409 } 6410 }); 6411 6412 ui.showDialog(self.$dialog); 6413 }); 6414 }; 6415 }; 6416 6417 var HelpDialog = function (context) { 6418 var self = this; 6419 var ui = $.summernote.ui; 6420 6421 var $editor = context.layoutInfo.editor; 6422 var options = context.options; 6423 var lang = options.langInfo; 6424 6425 this.createShortCutList = function () { 6426 var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc']; 6427 return Object.keys(keyMap).map(function (key) { 6428 var command = keyMap[key]; 6429 var $row = $('<div><div class="help-list-item"/></div>'); 6430 $row.append($('<label><kbd>' + key + '</kdb></label>').css({ 6431 'width': 180, 6432 'margin-right': 10 6433 })).append($('<span/>').html(context.memo('help.' + command) || command)); 6434 return $row.html(); 6435 }).join(''); 6436 }; 6437 6438 this.initialize = function () { 6439 var $container = options.dialogsInBody ? $(document.body) : $editor; 6440 6441 var body = [ 6442 '<p class="text-center">', 6443 '<a href="http://summernote.org/" target="_blank">Summernote 0.8.2</a> · ', 6444 '<a href="https://github.com/summernote/summernote" target="_blank">Project</a> · ', 6445 '<a href="https://github.com/summernote/summernote/issues" target="_blank">Issues</a>', 6446 '</p>' 6447 ].join(''); 6448 6449 this.$dialog = ui.dialog({ 6450 title: lang.options.help, 6451 fade: options.dialogsFade, 6452 body: this.createShortCutList(), 6453 footer: body, 6454 callback: function ($node) { 6455 $node.find('.modal-body').css({ 6456 'max-height': 300, 6457 'overflow': 'scroll' 6458 }); 6459 } 6460 }).render().appendTo($container); 6461 }; 6462 6463 this.destroy = function () { 6464 ui.hideDialog(this.$dialog); 6465 this.$dialog.remove(); 6466 }; 6467 6468 /** 6469 * show help dialog 6470 * 6471 * @return {Promise} 6472 */ 6473 this.showHelpDialog = function () { 6474 return $.Deferred(function (deferred) { 6475 ui.onDialogShown(self.$dialog, function () { 6476 context.triggerEvent('dialog.shown'); 6477 deferred.resolve(); 6478 }); 6479 ui.showDialog(self.$dialog); 6480 }).promise(); 6481 }; 6482 6483 this.show = function () { 6484 context.invoke('editor.saveRange'); 6485 this.showHelpDialog().then(function () { 6486 context.invoke('editor.restoreRange'); 6487 }); 6488 }; 6489 }; 6490 6491 var AirPopover = function (context) { 6492 var self = this; 6493 var ui = $.summernote.ui; 6494 6495 var options = context.options; 6496 6497 var AIR_MODE_POPOVER_X_OFFSET = 20; 6498 6499 this.events = { 6500 'summernote.keyup summernote.mouseup summernote.scroll': function () {
6501 self.update(); 6502 }, 6503 'summernote.change summernote.dialog.shown': function () { 6504 self.hide(); 6505 }, 6506 'summernote.focusout': function (we, e) { 6507 // [workaround] Firefox doesn't support relatedTarget on focusout 6508 // - Ignore hide action on focus out in FF. 6509 if (agent.isFF) { 6510 return; 6511 } 6512 6513 if (!e.relatedTarget || !dom.ancestor(e.relatedTarget, func.eq(self.$popover[0]))) { 6514 self.hide(); 6515 } 6516 } 6517 }; 6518 6519 this.shouldInitialize = function () { 6520 return options.airMode && !list.isEmpty(options.popover.air); 6521 }; 6522 6523 this.initialize = function () { 6524 this.$popover = ui.popover({ 6525 className: 'note-air-popover' 6526 }).render().appendTo('body'); 6527 var $content = this.$popover.find('.popover-content'); 6528 6529 context.invoke('buttons.build', $content, options.popover.air); 6530 }; 6531 6532 this.destroy = function () { 6533 this.$popover.remove(); 6534 }; 6535 6536 this.update = function () { 6537 var styleInfo = context.invoke('editor.currentStyle'); 6538 if (styleInfo.range && !styleInfo.range.isCollapsed()) { 6539 var rect = list.last(styleInfo.range.getClientRects()); 6540 if (rect) { 6541 var bnd = func.rect2bnd(rect); 6542 this.$popover.css({ 6543 display: 'block', 6544 left: Math.max(bnd.left + bnd.width / 2, 0) - AIR_MODE_POPOVER_X_OFFSET, 6545 top: bnd.top + bnd.height 6546 }); 6547 } 6548 } else { 6549 this.hide(); 6550 } 6551 }; 6552 6553 this.hide = function () { 6554 this.$popover.hide(); 6555 }; 6556 }; 6557 6558 var HintPopover = function (context) { 6559 var self = this; 6560 var ui = $.summernote.ui; 6561 6562 var POPOVER_DIST = 5; 6563 var hint = context.options.hint || []; 6564 var direction = context.options.hintDirection || 'bottom'; 6565 var hints = $.isArray(hint) ? hint : [hint]; 6566 6567 this.events = { 6568 'summernote.keyup': function (we, e) { 6569 if (!e.isDefaultPrevented()) { 6570 self.handleKeyup(e); 6571 } 6572 }, 6573 'summernote.keydown': function (we, e) { 6574 self.handleKeydown(e); 6575 }, 6576 'summernote.dialog.shown': function () { 6577 self.hide(); 6578 } 6579 }; 6580 6581 this.shouldInitialize = function () { 6582 return hints.length > 0; 6583 }; 6584 6585 this.initialize = function () { 6586 this.lastWordRange = null; 6587 this.$popover = ui.popover({ 6588 className: 'note-hint-popover', 6589 hideArrow: true, 6590 direction: '' 6591 }).render().appendTo('body'); 6592 6593 this.$popover.hide(); 6594 6595 this.$content = this.$popover.find('.popover-content'); 6596 6597 this.$content.on('click', '.note-hint-item', function () { 6598 self.$content.find('.active').removeClass('active'); 6599 $(this).addClass('active'); 6600 self.replace(); 6601 }); 6602 }; 6603 6604 this.destroy = function () { 6605 this.$popover.remove(); 6606 }; 6607 6608 this.selectItem = function ($item) { 6609 this.$content.find('.active').removeClass('active'); 6610 $item.addClass('active'); 6611 6612 this.$content[0].scrollTop = $item[0].offsetTop - (this.$content.innerHeight() / 2); 6613 }; 6614 6615 this.moveDown = function () { 6616 var $current = this.$content.find('.note-hint-item.active'); 6617 var $next = $current.next(); 6618 6619 if ($next.length) { 6620 this.selectItem($next); 6621 } else { 6622 var $nextGroup = $current.parent().next(); 6623 6624 if (!$nextGroup.length) { 6625 $nextGroup = this.$content.find('.note-hint-group').first(); 6626 } 6627 6628 this.selectItem($nextGroup.find('.note-hint-item').first()); 6629 } 6630 }; 6631 6632 this.moveUp = function () { 6633 var $current = this.$content.find('.note-hint-item.active'); 6634 var $prev = $current.prev(); 6635 6636 if ($prev.length) { 6637 this.selectItem($prev); 6638 } else { 6639 var $prevGroup = $current.parent().prev(); 6640 6641 if (!$prevGroup.length) { 6642 $prevGroup = this.$content.find('.note-hint-group').last(); 6643 } 6644 6645 this.selectItem($prevGroup.find('.note-hint-item').last()); 6646 } 6647 }; 6648 6649 this.replace = function () { 6650 var $item = this.$content.find('.note-hint-item.active'); 6651 6652 if ($item.length) { 6653 var node = this.nodeFromItem($item);
6654 this.lastWordRange.insertNode(node); 6655 range.createFromNode(node).collapse().select(); 6656 6657 this.lastWordRange = null; 6658 this.hide(); 6659 context.invoke('editor.focus'); 6660 } 6661 6662 }; 6663 6664 this.nodeFromItem = function ($item) { 6665 var hint = hints[$item.data('index')]; 6666 var item = $item.data('item'); 6667 var node = hint.content ? hint.content(item) : item; 6668 if (typeof node === 'string') { 6669 node = dom.createText(node); 6670 } 6671 return node; 6672 }; 6673 6674 this.createItemTemplates = function (hintIdx, items) { 6675 var hint = hints[hintIdx]; 6676 return items.map(function (item, idx) { 6677 var $item = $('<div class="note-hint-item"/>'); 6678 $item.append(hint.template ? hint.template(item) : item + ''); 6679 $item.data({ 6680 'index': hintIdx, 6681 'item': item 6682 }); 6683 6684 if (hintIdx === 0 && idx === 0) { 6685 $item.addClass('active'); 6686 } 6687 return $item; 6688 }); 6689 }; 6690 6691 this.handleKeydown = function (e) { 6692 if (!this.$popover.is(':visible')) { 6693 return; 6694 } 6695 6696 if (e.keyCode === key.code.ENTER) { 6697 e.preventDefault(); 6698 this.replace(); 6699 } else if (e.keyCode === key.code.UP) { 6700 e.preventDefault(); 6701 this.moveUp(); 6702 } else if (e.keyCode === key.code.DOWN) { 6703 e.preventDefault(); 6704 this.moveDown(); 6705 } 6706 }; 6707 6708 this.searchKeyword = function (index, keyword, callback) { 6709 var hint = hints[index]; 6710 if (hint && hint.match.test(keyword) && hint.search) { 6711 var matches = hint.match.exec(keyword); 6712 hint.search(matches[1], callback); 6713 } else { 6714 callback(); 6715 } 6716 }; 6717 6718 this.createGroup = function (idx, keyword) { 6719 var $group = $('<div class="note-hint-group note-hint-group-' + idx + '"/>'); 6720 this.searchKeyword(idx, keyword, function (items) { 6721 items = items || []; 6722 if (items.length) { 6723 $group.html(self.createItemTemplates(idx, items)); 6724 self.show(); 6725 } 6726 }); 6727 6728 return $group; 6729 }; 6730 6731 this.handleKeyup = function (e) { 6732 if (list.contains([key.code.ENTER, key.code.UP, key.code.DOWN], e.keyCode)) { 6733 if (e.keyCode === key.code.ENTER) { 6734 if (this.$popover.is(':visible')) { 6735 return; 6736 } 6737 } 6738 } else {
6739 var wordRange = context.invoke('editor.createRange').getWordRange(); 6740 var keyword = wordRange.toString(); 6741 if (hints.length && keyword) { 6742 this.$content.empty(); 6743 6744 var bnd = func.rect2bnd(list.last(wordRange.getClientRects())); 6745 if (bnd) { 6746 6747 this.$popover.hide(); 6748 6749 this.lastWordRange = wordRange; 6750 6751 hints.forEach(function (hint, idx) { 6752 if (hint.match.test(keyword)) { 6753 self.createGroup(idx, keyword).appendTo(self.$content); 6754 } 6755 }); 6756 6757 // set position for popover after group is created 6758 if (direction === 'top') { 6759 this.$popover.css({ 6760 left: bnd.left, 6761 top: bnd.top - this.$popover.outerHeight() - POPOVER_DIST 6762 }); 6763 } else { 6764 this.$popover.css({ 6765 left: bnd.left, 6766 top: bnd.top + bnd.height + POPOVER_DIST 6767 }); 6768 } 6769 6770 } 6771 } else { 6772 this.hide(); 6773 } 6774 } 6775 }; 6776 6777 this.show = function () { 6778 this.$popover.show(); 6779 }; 6780 6781 this.hide = function () { 6782 this.$popover.hide(); 6783 }; 6784 }; 6785 6786 6787 $.summernote = $.extend($.summernote, { 6788 version: '0.8.2', 6789 ui: ui, 6790 dom: dom, 6791 6792 plugins: {}, 6793 6794 options: { 6795 modules: { 6796 'editor': Editor, 6797 'clipboard': Clipboard, 6798 'dropzone': Dropzone, 6799 'codeview': Codeview, 6800 'statusbar': Statusbar, 6801 'fullscreen': Fullscreen, 6802 'handle': Handle, 6803 // FIXME: HintPopover must be front of autolink 6804 // - Script error about range when Enter key is pressed on hint popover 6805 'hintPopover': HintPopover, 6806 'autoLink': AutoLink, 6807 'autoSync': AutoSync, 6808 'placeholder': Placeholder, 6809 'buttons': Buttons, 6810 'toolbar': Toolbar, 6811 'linkDialog': LinkDialog, 6812 'linkPopover': LinkPopover, 6813 'imageDialog': ImageDialog, 6814 'imagePopover': ImagePopover, 6815 'videoDialog': VideoDialog, 6816 'helpDialog': HelpDialog, 6817 'airPopover': AirPopover 6818 }, 6819 6820 buttons: {}, 6821 6822 lang: 'en-US', 6823 6824 // toolbar 6825 toolbar: [ 6826 ['style', ['style']], 6827 ['font', ['bold', 'underline', 'clear']], 6828 ['fontname', ['fontname']], 6829 ['color', ['color']], 6830 ['para', ['ul', 'ol', 'paragraph']], 6831 ['table', ['table']], 6832 ['insert', ['link', 'picture', 'video']], 6833 ['view', ['fullscreen', 'codeview', 'help']] 6834 ], 6835 6836 // popover 6837 popover: { 6838 image: [ 6839 ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']], 6840 ['float', ['floatLeft', 'floatRight', 'floatNone']], 6841 ['remove', ['removeMedia']] 6842 ], 6843 link: [ 6844 ['link', ['linkDialogShow', 'unlink']] 6845 ], 6846 air: [ 6847 ['color', ['color']], 6848 ['font', ['bold', 'underline', 'clear']], 6849 ['para', ['ul', 'paragraph']], 6850 ['table', ['table']], 6851 ['insert', ['link', 'picture']] 6852 ] 6853 }, 6854 6855 // air mode: inline editor 6856 airMode: false, 6857 6858 width: null, 6859 height: null, 6860 6861 focus: false, 6862 tabSize: 4, 6863 styleWithSpan: true, 6864 shortcuts: true, 6865 textareaAutoSync: true, 6866 direction: null, 6867 6868 styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], 6869 6870 fontNames: [ 6871 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 6872 'Helvetica Neue', 'Helvetica', 'Impact', 'Lucida Grande', 6873 'Tahoma', 'Times New Roman', 'Verdana' 6874 ], 6875 6876 fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'], 6877 6878 // pallete colors(n x n) 6879 colors: [ 6880 ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'], 6881 ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'], 6882 ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'], 6883 ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'], 6884 ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'], 6885 ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'], 6886 ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'], 6887 ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031'] 6888 ], 6889 6890 lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'], 6891 6892 tableClassName: 'table table-bordered', 6893 6894 insertTableMaxSize: { 6895 col: 10, 6896 row: 10 6897 }, 6898 6899 dialogsInBody: false, 6900 dialogsFade: false, 6901 6902 maximumImageFileSize: null, 6903 6904 callbacks: { 6905 onInit: null, 6906 onFocus: null, 6907 onBlur: null, 6908 onEnter: null, 6909 onKeyup: null, 6910 onKeydown: null, 6911 onImageUpload: null, 6912 onImageUploadError: null 6913 }, 6914 6915 codemirror: { 6916 mode: 'text/html', 6917 htmlMode: true, 6918 lineNumbers: true 6919 }, 6920 6921 keyMap: { 6922 pc: { 6923 'ENTER': 'insertParagraph', 6924 'CTRL+Z': 'undo', 6925 'CTRL+Y': 'redo', 6926 'TAB': 'tab', 6927 'SHIFT+TAB': 'untab', 6928 'CTRL+B': 'bold', 6929 'CTRL+I': 'italic', 6930 'CTRL+U': 'underline', 6931 'CTRL+SHIFT+S': 'strikethrough', 6932 'CTRL+BACKSLASH': 'removeFormat', 6933 'CTRL+SHIFT+L': 'justifyLeft', 6934 'CTRL+SHIFT+E': 'justifyCenter', 6935 'CTRL+SHIFT+R': 'justifyRight', 6936 'CTRL+SHIFT+J': 'justifyFull', 6937 'CTRL+SHIFT+NUM7': 'insertUnorderedList', 6938 'CTRL+SHIFT+NUM8': 'insertOrderedList', 6939 'CTRL+LEFTBRACKET': 'outdent', 6940 'CTRL+RIGHTBRACKET': 'indent', 6941 'CTRL+NUM0': 'formatPara', 6942 'CTRL+NUM1': 'formatH1', 6943 'CTRL+NUM2': 'formatH2', 6944 'CTRL+NUM3': 'formatH3', 6945 'CTRL+NUM4': 'formatH4', 6946 'CTRL+NUM5': 'formatH5', 6947 'CTRL+NUM6': 'formatH6', 6948 'CTRL+ENTER': 'insertHorizontalRule', 6949 'CTRL+K': 'linkDialog.show' 6950 }, 6951 6952 mac: { 6953 'ENTER': 'insertParagraph', 6954 'CMD+Z': 'undo', 6955 'CMD+SHIFT+Z': 'redo', 6956 'TAB': 'tab', 6957 'SHIFT+TAB': 'untab', 6958 'CMD+B': 'bold', 6959 'CMD+I': 'italic', 6960 'CMD+U': 'underline', 6961 'CMD+SHIFT+S': 'strikethrough', 6962 'CMD+BACKSLASH': 'removeFormat', 6963 'CMD+SHIFT+L': 'justifyLeft', 6964 'CMD+SHIFT+E': 'justifyCenter', 6965 'CMD+SHIFT+R': 'justifyRight', 6966 'CMD+SHIFT+J': 'justifyFull', 6967 'CMD+SHIFT+NUM7': 'insertUnorderedList', 6968 'CMD+SHIFT+NUM8': 'insertOrderedList', 6969 'CMD+LEFTBRACKET': 'outdent', 6970 'CMD+RIGHTBRACKET': 'indent', 6971 'CMD+NUM0': 'formatPara', 6972 'CMD+NUM1': 'formatH1', 6973 'CMD+NUM2': 'formatH2', 6974 'CMD+NUM3': 'formatH3', 6975 'CMD+NUM4': 'formatH4', 6976 'CMD+NUM5': 'formatH5', 6977 'CMD+NUM6': 'formatH6', 6978 'CMD+ENTER': 'insertHorizontalRule', 6979 'CMD+K': 'linkDialog.show' 6980 } 6981 }, 6982 icons: { 6983 'align': 'note-icon-align', 6984 'alignCenter': 'note-icon-align-center', 6985 'alignJustify': 'note-icon-align-justify', 6986 'alignLeft': 'note-icon-align-left', 6987 'alignRight': 'note-icon-align-right', 6988 'indent': 'note-icon-align-indent', 6989 'outdent': 'note-icon-align-outdent', 6990 'arrowsAlt': 'note-icon-arrows-alt', 6991 'bold': 'note-icon-bold', 6992 'caret': 'note-icon-caret', 6993 'circle': 'note-icon-circle', 6994 'close': 'note-icon-close', 6995 'code': 'note-icon-code', 6996 'eraser': 'note-icon-eraser', 6997 'font': 'note-icon-font', 6998 'frame': 'note-icon-frame', 6999 'italic': 'note-icon-italic', 7000 'link': 'note-icon-link', 7001 'unlink': 'note-icon-chain-broken', 7002 'magic': 'note-icon-magic', 7003 'menuCheck': 'note-icon-check', 7004 'minus': 'note-icon-minus', 7005 'orderedlist': 'note-icon-orderedlist', 7006 'pencil': 'note-icon-pencil', 7007 'picture': 'note-icon-picture', 7008 'question': 'note-icon-question', 7009 'redo': 'note-icon-redo', 7010 'square': 'note-icon-square', 7011 'strikethrough': 'note-icon-strikethrough', 7012 'subscript': 'note-icon-subscript', 7013 'superscript': 'note-icon-superscript', 7014 'table': 'note-icon-table', 7015 'textHeight': 'note-icon-text-height', 7016 'trash': 'note-icon-trash',
7017 'underline': 'note-icon-underline', 7018 'undo': 'note-icon-undo', 7019 'unorderedlist': 'note-icon-unorderedlist', 7020 'video': 'note-icon-video' 7021 } 7022 } 7023 }); 7024 7025}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.