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