1/*! wysiwyg.js | https://github.com/wysiwygjs/wysiwyg.js | Apache License (v2) */ 2 3(function(window, document, undefined) 4 5{ 6 7 'use strict'; 8 9 10 11 // http://stackoverflow.com/questions/97962/debounce-clicks-when-submitting-a-web-form 12 13 var debounce = function( callback, wait, cancelprevious ) 14 15 { 16 17 var timeout; 18 19 return function() 20 21 { 22 23 if( timeout ) 24 25 { 26 27 if( ! cancelprevious ) 28 29 return; 30 31 clearTimeout( timeout ); 32 33 } 34 35 var context = this, 36 37 args = arguments; 38 39 timeout = setTimeout( 40 41 function() 42 43 { 44 45 timeout = null; 46 47 callback.apply( context, args ); 48 49 }, wait ); 50 51 }; 52 53 }; 54 55 56 57 // http://stackoverflow.com/questions/12949590/how-to-detach-event-in-ie-6-7-8-9-using-javascript 58 59 var addEvent = function( element, type, handler, useCapture ) 60 61 { 62 63 element.addEventListener( type, handler, useCapture ? true : false ); 64 65 }; 66 67 var removeEvent = function( element, type, handler, useCapture ) 68 69 { 70 71 element.removeEventListener( type, handler, useCapture ? true : false ); 72 73 }; 74 75 // prevent default 76 77 var cancelEvent = function( e ) 78 79 { 80 81 e.preventDefault(); 82 83 e.stopPropagation(); 84 85 }; 86 87 88 89 // http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another 90 91 var isOrContainsNode = function( ancestor, descendant, within ) 92 93 { 94 95 var node = within ? descendant.parentNode : descendant; 96 97 while( node ) 98 99 { 100 101 if( node === ancestor ) 102 103 return true; 104 105 node = node.parentNode; 106 107 } 108 109 return false; 110 111 }; 112 113 var isMediaNode = function( node ) 114 115 { 116 117 var name = node.nodeName; 118 119 return name == 'IMG' || name == 'PICTURE' || name == 'SVG' || name == 'VIDEO' || name == 'AUDIO' || 120 121 name == 'IFRAME' || name == 'MAP' || name == 'OBJECT' || name == 'EMBED'; 122 123 }; 124 125 126 127 // save/restore selection 128 129 // http://stackoverflow.com/questions/13949059/persisting-the-changes-of-range-objects-after-selection-in-html/13950376#13950376 130 131 var saveSelection = function( containerNode ) 132 133 { 134 135 var sel = window.getSelection(); 136 137 if( sel.rangeCount > 0 ) 138 139 return sel.getRangeAt(0); 140 141 return null; 142 143 }; 144 145 var restoreSelection = function( containerNode, savedSel ) 146 147 { 148 149 if( ! savedSel ) 150 151 return; 152 153 var sel = window.getSelection(); 154 155 sel.removeAllRanges(); 156 157 sel.addRange(savedSel); 158 159 }; 160 161 162 163 // http://stackoverflow.com/questions/12603397/calculate-width-height-of-the-selected-text-javascript 164 165 // http://stackoverflow.com/questions/6846230/coordinates-of-selected-text-in-browser-page 166 167 var getSelectionRect = function() 168 169 { 170 171 var sel = window.getSelection(); 172 173 if( ! sel.rangeCount ) 174 175 return false; 176 177 var range = sel.getRangeAt(0).cloneRange(); 178 179 var rect = range.getBoundingClientRect(); 180 181 // Safari 5.1 returns null, IE9 returns 0/0/0/0 if image selected 182 183 if( rect && rect.left && rect.top && rect.right && rect.bottom ) 184 185 return { 186 187 // Modern browsers return floating-point numbers 188 189 left: parseInt(rect.left), 190 191 top: parseInt(rect.top), 192 193 width: parseInt(rect.right - rect.left), 194 195 height: parseInt(rect.bottom - rect.top) 196 197 }; 198 199 // on Webkit 'range.getBoundingClientRect()' sometimes return 0/0/0/0 - but 'range.getClientRects()' works 200 201 var rects = range.getClientRects ? range.getClientRects() : []; 202 203 for( var i=0; i < rects.length; ++i ) 204 205 { 206 207 var rect = rects[i]; 208 209 if( rect.left && rect.top && rect.right && rect.bottom ) 210 211 return { 212 213 // Modern browsers return floating-point numbers 214 215 left: parseInt(rect.left), 216 217 top: parseInt(rect.top), 218 219 width: parseInt(rect.right - rect.left), 220 221 height: parseInt(rect.bottom - rect.top) 222 223 }; 224 225 } 226 227 return false;
228 229 }; 230 231 232 233 var getSelectionCollapsed = function( containerNode ) 234 235 { 236 237 var sel = window.getSelection(); 238 239 if( sel.isCollapsed ) 240 241 return true; 242 243 return false; 244 245 }; 246 247 248 249 // http://stackoverflow.com/questions/7781963/js-get-array-of-all-selected-nodes-in-contenteditable-div 250 251 var getSelectedNodes = function( containerNode ) 252 253 { 254 255 var sel = window.getSelection(); 256 257 if( ! sel.rangeCount ) 258 259 return []; 260 261 var nodes = []; 262 263 for( var i=0; i < sel.rangeCount; ++i ) 264 265 { 266 267 var range = sel.getRangeAt(i), 268 269 node = range.startContainer, 270 271 endNode = range.endContainer; 272 273 while( node ) 274 275 { 276 277 // add this node? 278 279 if( node != containerNode ) 280 281 { 282 283 var node_inside_selection = false; 284 285 if( sel.containsNode ) 286 287 node_inside_selection = sel.containsNode( node, true ); 288 289 else // IE11 290 291 { 292 293 // http://stackoverflow.com/questions/5884210/how-to-find-if-a-htmlelement-is-enclosed-in-selected-text 294 295 var noderange = document.createRange(); 296 297 noderange.selectNodeContents( node ); 298 299 for( var i=0; i < sel.rangeCount; ++i ) 300 301 { 302 303 var range = sel.getRangeAt(i); 304 305 // start after or end before -> skip node 306 307 if( range.compareBoundaryPoints(range.END_TO_START,noderange) >= 0 && 308 309 range.compareBoundaryPoints(range.START_TO_END,noderange) <= 0 ) 310 311 { 312 313 node_inside_selection = true; 314 315 break; 316 317 } 318 319 } 320 321 } 322 323 if( node_inside_selection ) 324 325 nodes.push( node ); 326 327 } 328 329 // http://stackoverflow.com/questions/667951/how-to-get-nodes-lying-inside-a-range-with-javascript 330 331 var nextNode = function( node, container ) 332 333 { 334 335 if( node.firstChild ) 336 337 return node.firstChild; 338 339 while( node ) 340 341 { 342 343 if( node == container ) // do not walk out of the container 344 345 return null; 346 347 if( node.nextSibling ) 348 349 return node.nextSibling; 350 351 node = node.parentNode; 352 353 } 354 355 return null; 356 357 }; 358 359 node = nextNode( node, node == endNode ? endNode : containerNode ); 360 361 } 362 363 } 364 365 // Fallback 366 367 if( nodes.length == 0 && isOrContainsNode(containerNode,sel.focusNode) && sel.focusNode != containerNode ) 368 369 nodes.push( sel.focusNode ); 370 371 return nodes; 372 373 }; 374 375 376 377 // http://stackoverflow.com/questions/8513368/collapse-selection-to-start-of-selection-not-div 378 379 var collapseSelectionEnd = function() 380 381 { 382 383 var sel = window.getSelection(); 384 385 if( ! sel.isCollapsed ) 386 387 { 388 389 // Form-submits via Enter throw 'NS_ERROR_FAILURE' on Firefox 34 390 391 try { 392 393 sel.collapseToEnd(); 394 395 } 396 397 catch( e ) { 398 399 } 400 401 } 402 403 }; 404 405 406 407 // http://stackoverflow.com/questions/15157435/get-last-character-before-caret-position-in-javascript 408 409 // http://stackoverflow.com/questions/11247737/how-can-i-get-the-word-that-the-caret-is-upon-inside-a-contenteditable-div 410 411 var expandSelectionCaret = function( containerNode, preceding, following ) 412 413 { 414 415 var sel = window.getSelection(); 416 417 if( sel.modify ) 418 419 { 420 421 for( var i=0; i < preceding; ++i ) 422 423 sel.modify('extend', 'backward', 'character'); 424 425 for( var i=0; i < following;
425 ++i ) 426 427 sel.modify('extend', 'forward', 'character'); 428 429 } 430 431 else 432 433 { 434 435 // not so easy if the steps would cover multiple nodes ... 436 437 var range = sel.getRangeAt(0); 438 439 range.setStart( range.startContainer, range.startOffset - preceding ); 440 441 range.setEnd( range.endContainer, range.endOffset + following ); 442 443 sel.removeAllRanges(); 444 445 sel.addRange(range); 446 447 } 448 449 }; 450 451 452 453 // http://stackoverflow.com/questions/4652734/return-html-from-a-user-selected-text/4652824#4652824 454 455 var getSelectionHtml = function( containerNode ) 456 457 { 458 459 if( getSelectionCollapsed( containerNode ) ) 460 461 return null; 462 463 var sel = window.getSelection(); 464 465 if( sel.rangeCount ) 466 467 { 468 469 var container = document.createElement('div'), 470 471 len = sel.rangeCount; 472 473 for( var i=0; i < len; ++i ) 474 475 { 476 477 var contents = sel.getRangeAt(i).cloneContents(); 478 479 container.appendChild(contents); 480 481 } 482 483 return container.innerHTML; 484 485 } 486 487 return null; 488 489 }; 490 491 492 493 var selectionInside = function( containerNode, force ) 494 495 { 496 497 // selection inside editor? 498 499 var sel = window.getSelection(); 500 501 if( isOrContainsNode(containerNode,sel.anchorNode) && isOrContainsNode(containerNode,sel.focusNode) ) 502 503 return true; 504 505 // selection at least partly outside editor 506 507 if( ! force ) 508 509 return false; 510 511 // force selection to editor 512 513 var range = document.createRange(); 514 515 range.selectNodeContents( containerNode ); 516 517 range.collapse( false ); 518 519 sel.removeAllRanges(); 520 521 sel.addRange(range); 522 523 return true; 524 525 }; 526 527 528 529 530 531 // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294 532 533 // http://stackoverflow.com/questions/4823691/insert-an-html-element-in-a-contenteditable-element 534 535 // http://stackoverflow.com/questions/6139107/programatically-select-text-in-a-contenteditable-html-element 536 537 var pasteHtmlAtCaret = function( containerNode, html ) 538 539 { 540 541 var sel = window.getSelection(); 542 543 if( sel.getRangeAt && sel.rangeCount ) 544 545 { 546 547 var range = sel.getRangeAt(0); 548 549 // Range.createContextualFragment() would be useful here but is 550 551 // only relatively recently standardized and is not supported in 552 553 // some browsers (IE9, for one) 554 555 var el = document.createElement('div'); 556 557 el.innerHTML = html; 558 559 var frag = document.createDocumentFragment(), node, lastNode; 560 561 while ( (node = el.firstChild) ) { 562 563 lastNode = frag.appendChild(node); 564 565 } 566 567 if( isOrContainsNode(containerNode, range.commonAncestorContainer) ) 568 569 { 570 571 range.deleteContents(); 572 573 range.insertNode(frag); 574 575 } 576 577 else { 578 579 containerNode.appendChild(frag); 580 581 } 582 583 // Preserve the selection 584 585 if( lastNode ) 586 587 { 588 589 range = range.cloneRange(); 590 591 range.setStartAfter(lastNode); 592 593 range.collapse(true); 594 595 sel.removeAllRanges(); 596 597 sel.addRange(range); 598 599 } 600 601 } 602 603 }; 604 605 606 607 // Create editor 608 609 var wysiwyg = function( element, options ) 610 611 { 612 613 var toolbar = options.toolbar, 614 615 buttons = options.buttons, selectionbuttons = options.selectionbuttons, 616 617 suggester = options.suggester, interceptenter = options.interceptenter, hijackContextmenu = !!options.hijackmenu, 618 619 node_container = typeof(element) == 'string' ? document.querySelector(element) : element, 620 621 node_textarea = node_container.querySelector('textarea'), 622 623 commands, hotkeys = {}, 624 625 toolbar_top = toolbar == 'top', toolbar_bottom = toolbar == 'bottom', toolbar_demand = toolbar == 'demand'; 626 627 628 629 // initialize editor 630 631 var add_class = function( element, classname ) 632 633 { 634 635 if( element.classList ) 636 637 element.classList.add( classname ); 638 639 else // IE9 640 641 element.className += ' ' + classname; 642 643 }; 644 645 var remove_class = function( element, classname ) 646 647 { 648 649 if( element.classList ) 650 651 element.classList.remove( classname ); 652 653 } 654 655 var node_contenteditable = node_container.querySelector('[contenteditable=true]'); 656 657 if( ! node_contenteditable ) 658 659 { 660 661 node_contenteditable = document.createElement( 'div' ); 662 663 node_contenteditable.setAttribute( 'contentEditable', 'true' ); 664 665 var placeholder = node_textarea.placeholder; 666 667 if( placeholder ) 668
669 node_contenteditable.setAttribute( 'data-placeholder', placeholder ); 670 671 node_container.insertBefore( node_contenteditable, node_container.firstChild ); 672 673 } 674 675 676 677 // Simulate ':focus-within' 678 679 var remove_focus_timeout = null; 680 681 var add_class_focus = function() 682 683 { 684 685 if( remove_focus_timeout ) 686 687 clearTimeout( remove_focus_timeout ); 688 689 remove_focus_timeout = null; 690 691 add_class( node_container, 'focus' ); 692 693 if( toolbar_demand ) 694 695 add_class( node_container, 'focused' ); 696 697 }; 698 699 var remove_class_focus = function() 700 701 { 702 703 if( remove_focus_timeout || document.activeElement == node_contenteditable ) 704 705 return ; 706 707 remove_focus_timeout = setTimeout( function() { 708 709 remove_focus_timeout = null; 710 711 remove_class( node_container, 'focus' ); 712 713 }, 50 ); 714 715 }; 716 717 addEvent( node_contenteditable, 'focus', add_class_focus ); 718 719 addEvent( node_contenteditable, 'blur', remove_class_focus ); 720 721 // register form-reset 722 723 if( node_textarea && node_textarea.form ) 724 725 addEvent( node_textarea.form, 'reset', remove_class_focus ); 726 727 728 729 // Insert-link popup 730 731 var create_insertlink = function( popup, modify_a_href ) 732 733 { 734 735 var textbox = document.createElement('input'); 736 737 textbox.placeholder = 'www.example.com'; 738 739 if( modify_a_href ) 740 741 textbox.value = modify_a_href.href; 742 743 textbox.style.width = '20em'; 744 745 textbox.style.maxWidth = parseInt(node_container.offsetWidth *2/3) + 'px'; 746 747 textbox.autofocus = true; 748 749 if( modify_a_href ) 750 751 addEvent( textbox, 'input', function( e ) 752 753 { 754 755 var url = textbox.value.trim(); 756 757 if( url ) 758 759 modify_a_href.href = url; 760 761 }); 762 763 addEvent( textbox, 'keypress', function( e ) 764 765 { 766 767 var key = e.which || e.keyCode; 768 769 if( key != 13 ) 770 771 return; 772 773 var url = textbox.value.trim(); 774 775 if( modify_a_href ) 776 777 ; 778 779 else if( url ) 780 781 { 782 783 var url_scheme = url; 784 785 if( ! /^[a-z0-9]+:\/\//.test(url) ) 786 787 url_scheme = "http://" + url; 788 789 if( commands.getSelectedHTML() ) 790 791 commands.insertLink( url_scheme ); 792 793 else 794 795 { 796 797 // Encode html entities - http://stackoverflow.com/questions/5499078/fastest-method-to-escape-html-tags-as-html-entities 798 799 var htmlencode = function( text ) { 800 801 return text.replace(/[&<>"]/g, function(tag) 802 803 { 804 805 var charsToReplace = { '&':'&', '<':'<', '>':'>', '"':'"' }; 806 807 return charsToReplace[tag] || tag; 808 809 }); 810 811 } 812 813 commands.insertHTML( '<a href="' + htmlencode(url_scheme) + '">' + htmlencode(url) + '</a>' ); 814 815 } 816 817 } 818 819 commands.closePopup().collapseSelection(); 820 821 node_contenteditable.focus(); 822 823 }); 824 825 popup.appendChild( textbox ); 826 827 // set focus 828 829 window.setTimeout( function() 830 831 { 832 833 textbox.focus(); 834 835 add_class_focus(); 836 837 }, 1 ); 838 839 }; 840 841 842 843 // Color-palette popup 844 845 var create_colorpalette = function( popup, forecolor ) 846 847 { 848 849 // http://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately 850 851 var HSVtoRGB = function( h, s, v ) 852 853 { 854 855 var r, g, b, i, f, p, q, t; 856 857 i = Math.floor(h * 6); 858 859 f = h * 6 - i; 860 861 p = v * (1 - s); 862 863 q = v * (1 - f * s); 864 865 t = v * (1 - (1 - f) * s); 866 867 switch (i % 6) 868 869 { 870 871 case 0: r = v, g = t, b = p; break; 872 873 case 1: r = q, g = v, b = p; break; 874 875 case 2: r = p, g = v, b = t; break; 876 877 case 3: r = p, g = q, b = v; break; 878 879 case 4: r = t, g = p, b = v; break; 880 881 case 5: r = v, g = p, b = q; break; 882 883 } 884 885 var hr = Math.floor(r * 255).toString(16); 886 887 var hg = Math.floor(g * 255).toString(16); 888 889 var hb = Math.floor(b * 255).toString(16); 890 891 return '#' + (hr.length < 2 ? '0' : '') + hr + 892 893 (hg.length < 2 ? '0' : '') + hg + 894 895 (hb.length < 2 ? '0' : '') + hb; 896 897 }; 898 899 // create table 900 901 var table = document.createElement('table'); 902 903 table.style.borderCollapse = 'collapse'; 904 905 for( var row=1; row < 15; ++row ) // should be '16' - but last line looks so dark 906 907 { 908 909 var tr = document.createElement('tr'); 910 911 for( var col=0; col < 25; ++col ) // last column is grayscale 912 913 { 914 915 var color; 916 917 if( col == 24 ) 918 919 { 920 921 var gray = Math.floor(255 / 13 * (14 - row)).toString(16); 922 923 var hexg = (gray.length < 2 ? '0' : '') + gray; 924 925 color = '#' + hexg + hexg + hexg; 926 927 } 928 929 else 930 931 { 932 933 var hue = col / 24; 934 935 var saturation = row <= 8 ? row /8 : 1; 936 937 var value = row > 8 ? (16-row)/8 : 1; 938 939 color = HSVtoRGB( hue, saturation, value ); 940 941 } 942 943 var td = document.createElement('td'); 944 945 td.style.width = '0.5em'; 946 947 td.style.height = '0.5em'; 948 949 td.style.cursor = 'pointer'; 950 951 td.style.backgroundColor = color; 952 953 td.title = color; 954 955 td.style.userSelect = 'none'; 956 957 addEvent( td, 'click', function( e ) 958 959 { 960 961 var color = this.title; 962 963 if( forecolor ) 964 965 commands.forecolor( color ).closePopup().collapseSelection(); 966 967 else 968 969 commands.highlight( color ).closePopup().collapseSelection(); 970 971 cancelEvent( e ); 972 973 }); 974 975 tr.appendChild( td ); 976 977 } 978 979 table.appendChild( tr ); 980 981 } 982 983 popup.appendChild( table ); 984 985 }; 986 987 988
989 // read file as data-url 990 991 var filecontents = function( file, callback ) 992 993 { 994 995 // base64 a 2GB video is insane: artificial clamp at 64MB 996 997 if( file.size > 0x4000000 ) 998 999 { 1000 1001 callback(); 1002 1003 return; 1004 1005 } 1006 1007 1008 1009 // read file as data-url 1010 1011 var normalize_dataurl = function( orientation ) 1012 1013 { 1014 1015 var filereader = new FileReader(); 1016 1017 filereader.onload = function( e ) 1018 1019 { 1020 1021 if( ! orientation || orientation == 1 || orientation > 8 ) 1022 1023 return callback( file.type, e.target.result ); 1024 1025 // normalize 1026 1027 var img = new Image(); 1028 1029 img.src = e.target.result; 1030 1031 img.onload = function() 1032 1033 { 1034 1035 var width = img.width; 1036 1037 var height = img.height; 1038 1039 if( width > height ) 1040 1041 { 1042 1043 var max_width = 4096; 1044 1045 if( width > max_width ) 1046 1047 { 1048 1049 height *= max_width / width; 1050 1051 width = max_width; 1052 1053 } 1054 1055 } 1056 1057 else 1058 1059 { 1060 1061 var max_height = 4096; 1062 1063 if( height > max_height ) 1064 1065 { 1066 1067 width *= max_height / height; 1068 1069 height = max_height; 1070 1071 } 1072 1073 } 1074 1075 var canvas = document.createElement( 'canvas' ); 1076 1077 var ctx = canvas.getContext( '2d' ); 1078 1079 canvas.width = width; 1080 1081 canvas.height = height; 1082 1083 ctx.save(); 1084 1085 if( orientation > 4 ) 1086 1087 { 1088 1089 canvas.width = height; 1090 1091 canvas.height = width; 1092 1093 } 1094 1095 switch( orientation ) 1096 1097 { 1098 1099 case 2: ctx.translate( width, 0 ); 1100 1101 ctx.scale( -1, 1 ); 1102 1103 break; 1104 1105 case 3: ctx.translate( width, height ); 1106 1107 ctx.rotate( Math.PI ); 1108 1109 break; 1110 1111 case 4: ctx.translate( 0, height ); 1112 1113 ctx.scale( 1, -1 ); 1114 1115 break; 1116 1117 case 5: ctx.rotate( 0.5 * Math.PI ); 1118 1119 ctx.scale( 1, -1 ); 1120 1121 break; 1122 1123 case 6: ctx.rotate( 0.5 * Math.PI ); 1124 1125 ctx.translate( 0, -height ); 1126 1127 break; 1128 1129 case 7: ctx.rotate( 0.5 * Math.PI ); 1130 1131 ctx.translate( width, -height ); 1132 1133 ctx.scale( -1, 1 ); 1134 1135 break; 1136 1137 case 8: ctx.rotate( -0.5 * Math.PI ); 1138 1139 ctx.translate( -width, 0 ); 1140 1141 break; 1142 1143 } 1144 1145 ctx.drawImage( img, 0, 0, width, height ); 1146 1147 ctx.restore(); 1148 1149 var dataURL = canvas.toDataURL( 'image/jpeg', 0.99 ); 1150 1151 callback( file.type, dataURL ); 1152 1153 } 1154 1155 }; 1156 1157 filereader.onerror = function( e ) 1158 1159 { 1160 1161 callback(); 1162 1163 }; 1164 1165 filereader.readAsDataURL( file ); 1166 1167 } 1168 1169 if( ! window.DataView ) 1170 1171 return normalize_dataurl(); 1172 1173 1174 1175 // get orientation - https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side 1176 1177 var filereader = new FileReader(); 1178 1179 filereader.onload = function( e ) 1180 1181 { 1182 1183 var contents = e.target.result; 1184 1185 var view = new DataView( contents ); 1186 1187 // Not a JPEG at all? 1188 1189 if( view.getUint16(0, false) != 0xFFD8 ) 1190 1191 return normalize_dataurl(); 1192 1193 var length = view.byteLength, 1194 1195 offset = 2; 1196 1197 while( offset < length ) 1198 1199 { 1200 1201 // Missing EXIF? 1202 1203 if( view.getUint16(offset+2, false) <= 8 ) 1204 1205 return normalize_dataurl(); 1206 1207 var marker = view.getUint16(offset, false); 1208 1209 offset += 2; 1210 1211 if( marker == 0xFFE1 ) 1212 1213 { 1214 1215 if( view.getUint32(offset += 2, false) != 0x45786966 ) 1216 1217 return normalize_dataurl(); 1218 1219 var little = view.getUint16( offset += 6, false ) == 0x4949; 1220 1221 offset += view.getUint32( offset + 4, little ); 1222 1223 var tags = view.getUint16( offset, little ); 1224 1225 offset += 2; 1226 1227 for( var i=0; i < tags; ++i ) 1228 1229 { 1230 1231 if( view.getUint16(offset + (i * 12), little) == 0x0112 ) 1232 1233 { 1234 1235 var orientation = view.getUint16( offset + (i * 12) + 8, little ); 1236 1237 return normalize_dataurl( orientation ); 1238 1239 } 1240 1241 } 1242 1243 } 1244 1245 else if( (marker & 0xFF00) != 0xFF00 ) 1246 1247 break; 1248 1249 else 1250 1251 offset += view.getUint16( offset, false ); 1252 1253 } 1254 1255 return normalize_dataurl(); 1256 1257 }; 1258 1259 filereader.onerror = function( e ) 1260 1261 { 1262 1263 callback(); 1264 1265 }; 1266 1267 filereader.readAsArrayBuffer( file ); 1268 1269 }; 1270 1271 1272 1273 // open popup and apply position 1274 1275 var popup_position = function( popup, left, top ) // left+top relative to container 1276 1277 { 1278 1279 // Test parents, el.getBoundingClientRect() does not work within 'position:fixed' 1280 1281 var node = node_container, 1282 1283 popup_parent = node.offsetParent; 1284 1285 while( node ) 1286 1287 { 1288 1289 var node_style = getComputedStyle( node ); 1290 1291 if( node_style['position'] != 'static' ) 1292 1293 break; 1294 1295 left += node.offsetLeft; 1296 1297 top += node.offsetTop; 1298 1299 popup_parent = node; 1300 1301 node = node.offsetParent; 1302 1303 } 1304 1305 // Move popup as high as possible in the DOM tree 1306 1307 popup_parent.appendChild( popup ); 1308 1309 // Trim to viewport 1310 1311 var rect = popup_parent.getBoundingClientRect(); 1312 1313 var documentElement = document.documentElement; 1314 1315 var viewport_width = Math.min( window.innerWidth, Math.max(documentElement.offsetWidth, documentElement.scrollWidth) ); 1316 1317 var viewport_height = window.innerHeight; 1318 1319 var popup_width = popup.offsetWidth; // accurate to integer 1320 1321 var popup_height = popup.offsetHeight; 1322 1323 if( rect.left + left < 1 ) 1324 1325 left = 1 - rect.left; 1326 1327 else if( rect.left + left + popup_width > viewport_width - 1 ) 1328 1329 left = Math.max( 1 - rect.left, viewport_width - 1 - rect.left - popup_width ); 1330 1331 if( rect.top + top < 1 ) 1332 1333 top = 1 - rect.top; 1334 1335 else if( rect.top + top + popup_height > viewport_height - 1 ) 1336 1337 top = Math.max( 1 - rect.top, viewport_height - 1 - rect.top - popup_height ); 1338 1339 // Set offset 1340 1341 popup.style.left = parseInt(left) + 'px'; 1342 1343 popup.style.top = parseInt(top) + 'px'; 1344 1345 }; 1346 1347 // open popup and apply position 1348 1349 var popup_type = null; 1350 1351 var create_popup = function( down, type, create_content, argument ) 1352 1353 { 1354 1355 // popup already open? 1356 1357 var popup = commands.activePopup(); 1358 1359 if( popup && popup_type === type ) 1360 1361 { 1362 1363 remove_class( popup, 'animate-down' ); 1364 1365 remove_class( popup, 'animate-up' ); 1366 1367 } 1368 1369 else 1370 1371 { 1372 1373 // either run 'commands.closePopup().openPopup()' or remove children 1374 1375 popup = commands.openPopup(); 1376 1377 add_class( popup, 'wysiwyg-popup' ); 1378 1379 add_class( popup, down ? 'animate-down' : 'animate-up' ); 1380 1381 popup_type = type; 1382 1383 } 1384 1385 // re-fill content 1386 1387 while( popup.firstChild ) 1388 1389 popup.removeChild( popup.firstChild ); 1390 1391 create_content( popup, argument ); 1392 1393 return popup; 1394 1395 }; 1396 1397 var open_popup_button = function( button, type, create_content, argument ) 1398 1399 { 1400 1401 var popup = create_popup( toolbar_top ? true : false, t
1401ype, create_content, argument ); 1402 1403 // Popup position - point to top/bottom-center of the button 1404 1405 var container_offset = node_container.getBoundingClientRect(); 1406 1407 var button_offset = button.getBoundingClientRect(); 1408 1409 var left = button_offset.left - container_offset.left + 1410 1411 parseInt(button.offsetWidth / 2) - parseInt(popup.offsetWidth / 2); 1412 1413 var top = button_offset.top - container_offset.top; 1414 1415 if( toolbar_top ) 1416 1417 top += button.offsetHeight; 1418 1419 else 1420 1421 top -= popup.offsetHeight; 1422 1423 popup_position( popup, left, top ); 1424 1425 }; 1426 1427 var popup_selection_position = function( popup, rect ) 1428 1429 { 1430 1431 // Popup position - point to center of the selection 1432 1433 var container_offset = node_container.getBoundingClientRect(); 1434 1435 var contenteditable_offset = node_contenteditable.getBoundingClientRect(); 1436 1437 var left = rect.left + parseInt(rect.width / 2) - parseInt(popup.offsetWidth / 2) + contenteditable_offset.left - container_offset.left; 1438 1439 var top = rect.top + rect.height + contenteditable_offset.top - container_offset.top; 1440 1441 popup_position( popup, left, top ); 1442 1443 }; 1444 1445 var open_popup_selection = function( rect, type, create_content, argument ) 1446 1447 { 1448 1449 var popup = create_popup( true, type, create_content, argument ); 1450 1451 popup_selection_position( popup, rect ); 1452 1453 }; 1454 1455 1456 1457 // Fill buttons (on toolbar or on selection) 1458 1459 var recent_selection_rect = null, recent_selection_link = null 1460 1461 var fill_buttons = function( toolbar_container, selection_rect, buttons, hotkeys ) 1462 1463 { 1464 1465 buttons.forEach( function(button) 1466 1467 { 1468 1469 // Custom button 1470 1471 if( button instanceof HTMLElement ) 1472 1473 { 1474 1475 toolbar_container.appendChild( button ); 1476 1477 // Simulate ':focus-within' 1478 1479 addEvent( button, 'focus', add_class_focus ); 1480 1481 addEvent( button, 'blur', remove_class_focus ); 1482 1483 return; 1484 1485 } 1486 1487 1488 1489 // Create a button 1490 1491 var element = document.createElement( 'button' ); 1492 1493 add_class( element, 'btn' ); 1494 1495 if( 'icon' in button ) 1496 1497 { 1498 1499 var htmlparser = document.implementation.createHTMLDocument(''); 1500 1501 htmlparser.body.innerHTML = button.icon; 1502 1503 for( var child=htmlparser.body.firstChild; child !== null; child=child.nextSibling ) 1504 1505 element.appendChild( child ); 1506 1507 } 1508 1509 if( 'attr' in button ) 1510 1511 { 1512 1513 for( var key in button.attr ) 1514 1515 element.setAttribute( key, button.attr[key] ); 1516 1517 } 1518 1519 // Simulate ':focus-within' 1520 1521 addEvent( element, 'focus', add_class_focus ); 1522 1523 addEvent( element, 'blur', remove_class_focus ); 1524 1525 1526 1527 // Create handler 1528 1529 var handler = null; 1530 1531 if( 'click' in button ) 1532 1533 handler = function() 1534 1535 { 1536 1537 button.click( commands, element ); 1538 1539 }; 1540 1541 else if( 'popup' in button ) 1542 1543 handler = function() 1544 1545 { 1546 1547 var fill_popup = function( popup ) 1548 1549 { 1550 1551 button.popup( commands, popup, element ); 1552 1553 }; 1554 1555 if( selection_rect ) 1556 1557 open_popup_selection( selection_rect, fill_popup.toString(), fill_popup ); 1558 1559 else 1560 1561 open_popup_button( element, fill_popup.toString(), fill_popup ); 1562 1563 }; 1564 1565 else if( 'browse' in button || 'dataurl' in button ) 1566 1567 handler = function() 1568 1569 { 1570 1571 // remove popup 1572 1573 commands.closePopup().collapseSelection(); 1574 1575 // open browse dialog 1576 1577 var input = document.createElement( 'input' ); 1578 1579 input.type = 'file'; 1580 1581 input.multiple = true; 1582 1583 input.style.display = 'none'; 1584 1585 addEvent( input, 'change', function( e ) 1586 1587 { 1588 1589 var remove_input = 'dataurl' in button; 1590 1591 if( ! e.target.files ) 1592 1593 remove_input = true; 1594 1595 else 1596 1597 { 1598 1599 var files = evt.target.files; 1600 1601 for( var i=0; i < files.length; ++i ) // can't use forEach() with 'FileList' 1602 1603 { 1604 1605 var file = files[i]; 1606 1607 if( 'browse' in button ) 1608 1609 button.browse( commands, input,file, element ); 1610 1611 else 1612 1613 { 1614 1615 // Keep callback-order - supporting browser without 'Promise' 1616 1617 var callbacks = [], 1618 1619 callnext = 0; 1620 1621 (function(i) 1622 1623 { 1624 1625 filecontents( file, 1626 1627 function( type, dataurl ) 1628 1629 { 1630 1631 callbacks[i] = function() { 1632 1633 if( dataurl ) // empty on error 1634 1635 button.dataurl( commands, type, dataurl, element ); 1636 1637 }; 1638 1639 // trigger callbacks in order 1640 1641 while( callnext in callbacks ) 1642 1643 { 1644 1645 callbacks[callnext](); 1646 1647 callnext++; 1648 1649 } 1650 1651 if( callnext == files.length ) 1652 1653 callbacks = null; 1654 1655 }); 1656 1657 })(i); 1658 1659 } 1660 1661 } 1662 1663 } 1664 1665 if( remove_input ) 1666 1667 input.parentNode.removeChild( input ); 1668 1669 cancelEvent( e ); 1670 1671 }); 1672 1673 node_container.appendChild( input ); 1674 1675 var evt = document.createEvent( 'MouseEvents' ); 1676 1677 evt.initEvent( 'click', true, false ); 1678 1679 input.dispatchEvent( evt ); 1680 1681 }; 1682 1683 else if( 'action' in button ) 1684 1685 handler = function() 1686 1687 { 1688 1689 switch( button.action ) 1690 1691 { 1692 1693 case 'link': 1694 1695 if( selection_rect ) 1696 1697 open_popup_selection( selection_rect, 'link', create_insertlink, recent_selection_link ); 1698 1699 else 1700 1701 open_popup_button( element, 'link', create_insertlink, recent_selection_link ); 1702 1703 break; 1704 1705 case 'bold': 1706 1707 commands.bold().closePopup().collapseSelection(); 1708 1709 break; 1710 1711 case 'italic': 1712 1713 commands.italic().closePopup().collapseSelection(); 1714 1715 break; 1716 1717 case 'underline': 1718 1719 commands.underline().closePopup().collapseSelection(); 1720 1721 break; 1722 1723 case 'strikethrough': 1724 1725 commands.strikethrough().closePopup().collapseSelection(); 1726 1727 break; 1728 1729 case 'colortext': 1730 1731 if( selection_rect ) 1732 1733 open_popup_selection( selection_rect, 'colortext', create_colorpalette, true ); 1734 1735 else 1736 1737 open_popup_button( element, 'colortext', create_colorpalette, true ); 1738 1739 break; 1740 1741 case 'colorfill': 1742 1743 if( selection_rect ) 1744 1745 open_popup_selection( selection_rect, 'colorfill', create_colorpalette, false ); 1746 1747 else 1748 1749 open_popup_button( element, 'colorfill', create_colorpalette, false ); 1750 1751 break; 1752 1753 case 'subscript': 1754 1755 commands.subscript().closePopup().collapseSelection(); 1756 1757 break; 1758 1759 case 'superscript': 1760 1761 commands.superscript().closePopup().collapseSelection(); 1762 1763 break; 1764 1765 case 'orderedlist': 1766 1767 commands.orderedList().closePopup().collapseSelection(); 1768 1769 break; 1770 1771 case 'unorderedlist': 1772 1773 commands.unorderedList().closePopup().collapseSelection(); 1774 1775 break; 1776 1777 case 'clearformat': 1778 1779 commands.removeFormat().closePopup().collapseSelection(); 1780 1781 break; 1782 1783 } 1784 1785 }; 1786 1787 element.onclick = function( e ) 1788 1789 { 1790 1791 if( handler ) 1792 1793 handler(); 1794 1795 cancelEvent( e ); 1796 1797 }; 1798 1799 toolbar_container.appendChild( element ); 1800 1801 1802 1803 // Hotkey 1804 1805 if( 'hotkey' in button && handler && hotkeys ) 1806 1807 hotkeys[button.hotkey.toLowerCase()] = handler; 1808 1809 }); 1810 1811 }; 1812 1813 1814 1815 // Handle suggester 1816 1817 var typed_suggestion = null, suggestion_sequence = 1, first_suggestion_html = null; 1818 1819 var finish_suggestion = function( insert_html ) 1820 1821 { 1822 1823 // fire suggestion 1824 1825 if( insert_html ) 1826 1827 commands.expandSelection( typed_suggestion.length, 0 ).insertHTML( insert_html ); 1828 1829 typed_suggestion = null; 1830 1831 first_suggestion_html = null; 1832 1833 suggestion_sequence += 1; 1834 1835 commands.closePopup(); 1836 1837 }; 1838 1839 var suggester_keydown = function( key, character, shiftKey, altKey, ctrlKey, metaKey ) 1840 1841 { 1842 1843 if( key == 13 && first_suggestion_html ) 1844 1845 { 1846 1847 finish_suggestion( first_suggestion_html ); 1848 1849 return false; // swallow enter 1850 1851 } 1852 1853 return true; 1854 1855 }; 1856 1857 var ask_suggestions = function() 1858 1859 { 1860 1861 if( ! typed_suggestion ) 1862 1863 return; 1864 1865 var current_sequence = suggestion_sequence; 1866 1867 var open_suggester = function( suggestions ) 1868 1869 { 1870 1871 if( ! recent_selection_rect || current_sequence != suggestion_sequence ) 1872 1873 return; 1874 1875 first_suggestion_html = null; 1876 1877 // Empty suggestions means: stop suggestion handling 1878 1879 if( ! suggestions ) 1880 1881 { 1882 1883 finish_suggestion(); 1884 1885 return; 1886 1887 } 1888 1889 // Show suggester 1890 1891 var fill_popup = function( popup ) 1892 1893 { 1894 1895 suggestions.forEach( function( suggestion ) 1896 1897 { 1898 1899 var element = document.createElement('div'); 1900 1901 add_class( element, 'suggestion' ); 1902 1903 element.innerHTML = suggestion.label; 1904 1905 addEvent( element, 'click', function( e ) 1906 1907 { 1908 1909 finish_suggestion( suggestion.insert ); 1910 1911 cancelEvent( e ); 1912 1913 }); 1914 1915 popup.appendChild( element ); 1916 1917 1918 1919 // Store suggestion to handle 'Enter' 1920 1921 if( first_suggestion_html === null ) 1922 1923 first_suggestion_html = suggestion.insert; 1924 1925 }); 1926 1927 }; 1928 1929 open_popup_selection( recent_selection_rect, 'suggestion', fill_popup ); 1930 1931 }; 1932 1933 // Ask to start/continue a suggestion 1934 1935 if( ! suggester(typed_suggestion, open_suggester) ) 1936 1937 finish_suggestion(); 1938 1939 }; 1940
1941 var debounced_suggestions = debounce( ask_suggestions, 100, true ); 1942 1943 var suggester_keypress = function( key, character, shiftKey, altKey, ctrlKey, metaKey ) 1944 1945 { 1946 1947 // Special keys 1948 1949 switch( key ) 1950 1951 { 1952 1953 case 8: // backspace 1954 1955 if( typed_suggestion ) 1956 1957 typed_suggestion = typed_suggestion.slice( 0, -1 ); 1958 1959 if( typed_suggestion ) // if still text -> continue, else abort 1960 1961 break; 1962 1963 finish_suggestion(); 1964 1965 return true; 1966 1967 case 13: // enter 1968 1969 case 27: // escape 1970 1971 case 33: // pageUp 1972 1973 case 34: // pageDown 1974 1975 case 35: // end 1976 1977 case 36: // home 1978 1979 case 37: // left 1980 1981 case 38: // up 1982 1983 case 39: // right 1984 1985 case 40: // down 1986 1987 if( typed_suggestion ) 1988 1989 finish_suggestion(); 1990 1991 return true; 1992 1993 default: 1994 1995 if( ! typed_suggestion ) 1996 1997 typed_suggestion = ''; 1998 1999 typed_suggestion += character; 2000 2001 break; 2002 2003 } 2004 2005 // Throttle requests 2006 2007 debounced_suggestions(); 2008 2009 return true; 2010 2011 }; 2012 2013 2014 2015 // Create contenteditable 2016 2017 var onKeyDown = function( key, character, shiftKey, altKey, ctrlKey, metaKey ) 2018 2019 { 2020 2021 // submit form on enter-key 2022 2023 if( interceptenter && key == 13 && ! shiftKey && ! altKey && ! ctrlKey && ! metaKey ) 2024 2025 { 2026 2027 commands.sync(); 2028 2029 if( interceptenter() ) 2030 2031 { 2032 2033 commands.closePopup(); 2034 2035 return false; // swallow enter 2036 2037 } 2038 2039 } 2040 2041 // Exec hotkey (onkeydown because e.g. CTRL+B would oben the bookmarks) 2042 2043 if( character && !shiftKey && !altKey && ctrlKey && !metaKey ) 2044 2045 { 2046 2047 var hotkey = character.toLowerCase(); 2048 2049 if( hotkeys[hotkey] ) 2050 2051 { 2052 2053 hotkeys[hotkey](); 2054 2055 return false; // prevent default 2056 2057 } 2058 2059 } 2060 2061 // Handle suggester 2062 2063 if( suggester ) 2064 2065 return suggester_keydown( key, character, shiftKey, altKey, ctrlKey, metaKey ); 2066 2067 }; 2068 2069 var onKeyPress = function( key, character, shiftKey, altKey, ctrlKey, metaKey ) 2070 2071 { 2072 2073 // Handle suggester 2074 2075 if( suggester ) 2076 2077 return suggester_keypress( key, character, shiftKey, altKey, ctrlKey, metaKey ); 2078 2079 }; 2080 2081 //var onKeyUp = function( key, character, shiftKey, altKey, ctrlKey, metaKey ) 2082 2083 //{ 2084 2085 //}; 2086 2087 var onSelection = function( collapsed, rect, nodes, rightclick ) 2088 2089 { 2090 2091 recent_selection_rect = collapsed ? rect || recent_selection_rect : null; 2092 2093 recent_selection_link = null; 2094 2095 // Fix type error - https://github.com/wysiwygjs/wysiwyg.js/issues/4 2096 2097 if( ! rect ) 2098 2099 { 2100 2101 finish_suggestion(); 2102 2103 return; 2104 2105 } 2106 2107 // Collapsed selection 2108 2109 if( collapsed ) 2110 2111 { 2112 2113 // Active suggestion: apply toolbar-position 2114 2115 if( typed_suggestion !== null ) 2116 2117 { 2118 2119 var popup = commands.activePopup(); 2120 2121 if( popup ) 2122 2123 { 2124 2125 remove_class( popup, 'animate-down' ); 2126 2127 remove_class( popup, 'animate-up' ); 2128 2129 popup_selection_position( popup, rect ); 2130 2131 } 2132 2133 return; 2134 2135 } 2136 2137 } 2138 2139 // Click on a link opens the link-popup 2140 2141 for( var i=0; i < nodes.length; ++i ) 2142 2143 { 2144 2145 var node = nodes[i]; 2146
2147 var closest = node.closest || // latest 2148 2149 function( selector ) { // IE + Edge - https://github.com/nefe/You-Dont-Need-jQuery 2150 2151 var node = this; 2152 2153 while( node ) 2154 2155 { 2156 2157 var matchesSelector = node.matches || node.webkitMatchesSelector || node.mozMatchesSelector || node.msMatchesSelector; 2158 2159 if( matchesSelector && matchesSelector.call(node, selector) ) 2160 2161 return node; 2162 2163 node = node.parentElement; 2164 2165 } 2166 2167 return null; 2168 2169 }; 2170 2171 recent_selection_link = closest.call( node, 'a' ); 2172 2173 if( recent_selection_link ) 2174 2175 break; 2176 2177 } 2178 2179 // Show selection popup? 2180 2181 var show_popup = true; 2182 2183 // 'right-click' always opens the popup 2184 2185 if( rightclick ) 2186 2187 ; 2188 2189 // No selection-popup wanted? 2190 2191 else if( ! selectionbuttons ) 2192 2193 show_popup = false; 2194 2195 // Selected popup wanted, but nothing selected (=selection collapsed) 2196 2197 else if( collapsed ) 2198 2199 show_popup = false; 2200 2201 // Image selected -> skip toolbar-popup (better would be an 'image-popup') 2202 2203 else nodes.forEach( function(node) 2204 2205 { 2206 2207 if( isMediaNode(node) ) 2208 2209 show_popup = false; 2210 2211 }); 2212 2213 if( ! show_popup ) 2214 2215 { 2216 2217 finish_suggestion(); 2218 2219 return; 2220 2221 } 2222 2223 // fill buttons 2224 2225 open_popup_selection( rect, 'selection', function( popup ) 2226 2227 { 2228 2229 var toolbar_element = document.createElement('div'); 2230 2231 add_class( toolbar_element, 'toolbar' ); 2232 2233 popup.appendChild( toolbar_element ); 2234 2235 fill_buttons( toolbar_element, rect, selectionbuttons ); 2236 2237 }); 2238 2239 }; 2240 2241 var onOpenpopup = function() 2242 2243 { 2244 2245 add_class_focus(); 2246 2247 }; 2248 2249 var onClosepopup = function() 2250 2251 { 2252 2253 finish_suggestion(); 2254 2255 remove_class_focus(); 2256 2257 }; 2258 2259 2260 2261 // Sync Editor with Textarea 2262 2263 var syncTextarea = null, 2264 2265 debounced_syncTextarea = null, 2266 2267 callUpdates; 2268 2269 if( node_textarea ) 2270 2271 { 2272 2273 // copy placeholder from the textarea to the contenteditor 2274 2275 if( ! node_contenteditable.innerHTML && node_textarea.value ) 2276 2277 node_contenteditable.innerHTML = node_textarea.value; 2278 2279 2280 2281 // sync html from the contenteditor to the textarea 2282 2283 var previous_html = node_contenteditable.innerHTML; 2284 2285 syncTextarea = function() 2286 2287 { 2288 2289 var new_html = node_contenteditable.innerHTML; 2290 2291 if( new_html.match(/^<br[/ ]*>$/i) ) 2292 2293 { 2294 2295 node_contenteditable.innerHTML = ''; 2296 2297 new_html = ''; 2298 2299 } 2300 2301 if( new_html == previous_html ) 2302 2303 return; 2304 2305 // HTML changed 2306 2307 node_textarea.value = new_html; 2308 2309 previous_html = new_html; 2310 2311 }; 2312 2313 2314 2315 // Focus/Blur events 2316 2317 addEvent( node_contenteditable, 'focus', function() 2318 2319 { 2320 2321 // forward focus/blur to the textarea 2322 2323 var event = document.createEvent( 'Event' ); 2324 2325 event.initEvent( 'focus', false, false ); 2326 2327 node_textarea.dispatchEvent( event ); 2328 2329 }); 2330 2331 addEvent( node_contenteditable, 'blur', function() 2332 2333 { 2334 2335 // sync textarea immediately 2336 2337 syncTextarea(); 2338 2339 // forward focus/blur to the textarea 2340 2341 var event = document.createEvent( 'Event' ); 2342 2343 event.initEvent( 'blur', false, false ); 2344 2345 node_textarea.dispatchEvent( event ); 2346 2347 }); 2348 2349 2350 2351 // debounce 'syncTextarea', because 'innerHTML' is quite burdensome 2352 2353 // High timeout is save, because of "onblur" fires immediately 2354
2355 debounced_syncTextarea = debounce( syncTextarea, 250, true ); 2356 2357 2358 2359 // Catch change events 2360 2361 // http://stackoverflow.com/questions/1391278/contenteditable-change-events/1411296#1411296 2362 2363 // http://stackoverflow.com/questions/8694054/onchange-event-with-contenteditable/8694125#8694125 2364 2365 // https://github.com/mindmup/bootstrap-wysiwyg/pull/50/files 2366 2367 // http://codebits.glennjones.net/editing/events-contenteditable.htm 2368 2369 addEvent( node_contenteditable, 'input', debounced_syncTextarea ); 2370 2371 addEvent( node_contenteditable, 'propertychange', debounced_syncTextarea ); 2372 2373 addEvent( node_contenteditable, 'textInput', debounced_syncTextarea ); 2374 2375 addEvent( node_contenteditable, 'paste', debounced_syncTextarea ); 2376 2377 addEvent( node_contenteditable, 'cut', debounced_syncTextarea ); 2378 2379 addEvent( node_contenteditable, 'drop', debounced_syncTextarea ); 2380 2381 // MutationObserver should report everything 2382 2383 if( window.MutationObserver ) 2384 2385 { 2386 2387 var observer = new MutationObserver( debounced_syncTextarea ); 2388 2389 observer.observe( node_contenteditable, {attributes:true,childList:true,characterData:true,subtree:true}); 2390 2391 } 2392 2393 2394 2395 // handle reset event 2396 2397 var form = node_textarea.form; 2398 2399 if( form ) 2400 2401 { 2402 2403 addEvent( form, 'reset', function() { 2404 2405 node_contenteditable.innerHTML = ''; 2406 2407 debounced_syncTextarea(); 2408 2409 callUpdates( true ); 2410 2411 }); 2412 2413 } 2414 2415 } 2416 2417 2418 2419 // Handle selection 2420 2421 var popup_saved_selection = null, // preserve selection during popup 2422 2423 debounced_handleSelection = null; 2424 2425 if( onSelection ) 2426 2427 { 2428 2429 var handleSelection = function( clientX, clientY, rightclick ) 2430 2431 { 2432 2433 // Detect collapsed selection 2434 2435 var collapsed = getSelectionCollapsed( node_contenteditable ); 2436 2437 // List of all selected nodes 2438 2439 var nodes = getSelectedNodes( node_contenteditable ); 2440 2441 // Rectangle of the selection 2442 2443 var rect = (clientX === null || clientY === null) ? null : 2444 2445 { 2446 2447 left: clientX, 2448 2449 top: clientY, 2450 2451 width: 0, 2452 2453 height: 0 2454 2455 }; 2456 2457 var selectionRect = getSelectionRect(); 2458 2459 if( selectionRect ) 2460 2461 rect = selectionRect; 2462 2463 if( rect ) 2464 2465 { 2466 2467 // So far 'rect' is relative to viewport, make it relative to the editor 2468 2469 var boundingrect = node_contenteditable.getBoundingClientRect(); 2470 2471 rect.left -= parseInt(boundingrect.left); 2472 2473 rect.top -= parseInt(boundingrect.top); 2474 2475 // Trim rectangle to the editor 2476 2477 if( rect.left < 0 ) 2478 2479 rect.left = 0; 2480 2481 if( rect.top < 0 ) 2482 2483 rect.top = 0; 2484 2485 if( rect.width > node_contenteditable.offsetWidth ) 2486 2487 rect.width = node_contenteditable.offsetWidth; 2488 2489 if( rect.height > node_contenteditable.offsetHeight ) 2490 2491 rect.height = node_contenteditable.offsetHeight; 2492 2493 } 2494 2495 else if( nodes.length ) 2496 2497 { 2498 2499 // What else could we do? Offset of first element... 2500 2501 for( var i=0; i < nodes.length; ++i ) 2502 2503 { 2504 2505 var node = nodes[i]; 2506 2507 if( node.nodeType != Node.ELEMENT_NODE ) 2508 2509 continue; 2510 2511 rect = { 2512 2513 left: node.offsetLeft, 2514 2515 top: node.offsetTop, 2516 2517 width: node.offsetWidth, 2518 2519 height: node.offsetHeight 2520 2521 }; 2522 2523 break; 2524 2525 } 2526 2527 } 2528 2529 // Callback 2530 2531 onSelection( collapsed, rect, nodes, rightclick ); 2532 2533 }; 2534 2535 debounced_handleSelection = debounce( handleSelection, 1 ); 2536 2537 } 2538 2539 2540 2541 // Open popup 2542 2543 var node_popup = null; 2544 2545 var popupClickClose = function( e ) 2546 2547 { 2548 2549 var target = e.target || e.srcElement; 2550 2551 if( target.nodeType == Node.TEXT_NODE ) // defeat Safari bug 2552 2553 target = target.parentNode; 2554 2555 // Click within popup? 2556 2557 if( isOrContainsNode(node_popup,target) ) 2558 2559 return; 2560 2561 // close popup 2562 2563 popupClose(); 2564 2565 }; 2566 2567 var popupOpen = function() 2568 2569 { 2570 2571 // Already open? 2572 2573 if( node_popup ) 2574 2575 return node_popup; 2576 2577 2578 2579 // Global click closes popup 2580 2581 addEvent( window, 'mousedown', popupClickClose, true ); 2582 2583 2584 2585 // Create popup element 2586 2587 node_popup = document.createElement( 'DIV' ); 2588 2589 var parent = node_contenteditable.parentNode, 2590 2591 next = node_contenteditable.nextSibling; 2592 2593 if( next ) 2594 2595 parent.insertBefore( node_popup, next ); 2596 2597 else 2598 2599 parent.appendChild( node_popup ); 2600 2601 if( onOpenpopup ) 2602 2603 onOpenpopup(); 2604 2605 return node_popup; 2606 2607 }; 2608 2609 var popupClose = function() 2610 2611 { 2612 2613 if( ! node_popup ) 2614 2615 return; 2616 2617 node_popup.parentNode.removeChild( node_popup ); 2618 2619 node_popup = null; 2620 2621 removeEvent( window, 'mousedown', popupClickClose, true ); 2622 2623 if( onClosepopup ) 2624 2625 onClosepopup(); 2626 2627 }; 2628 2629 2630 2631 // Key events 2632 2633 // http://sandbox.thewikies.com/html5-experiments/key-events.html 2634 2635 var keyHandler = function( e, phase ) 2636 2637 { 2638 2639 // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent 2640 2641 // http://stackoverflow.com/questions/1444477/keycode-and-ch
2641arcode 2642 2643 // http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 2644 2645 // http://unixpapa.com/js/key.html 2646 2647 var key = e.which || e.keyCode, 2648 2649 character = String.fromCharCode(key || e.charCode), 2650 2651 shiftKey = e.shiftKey || false, 2652 2653 altKey = e.altKey || false, 2654 2655 ctrlKey = e.ctrlKey || false, 2656 2657 metaKey = e.metaKey || false; 2658 2659 if( phase == 1 ) 2660 2661 { 2662 2663 // Callback 2664 2665 if( onKeyDown && onKeyDown(key, character, shiftKey, altKey, ctrlKey, metaKey) === false ) 2666 2667 cancelEvent( e ); // dismiss key 2668 2669 } 2670 2671 else if( phase == 2 ) 2672 2673 { 2674 2675 // Callback 2676 2677 if( onKeyPress && onKeyPress(key, character, shiftKey, altKey, ctrlKey, metaKey) === false ) 2678 2679 cancelEvent( e ); // dismiss key 2680 2681 } 2682 2683 //else if( phase == 3 ) 2684 2685 //{ 2686 2687 // // Callback 2688 2689 // if( onKeyUp && onKeyUp(key, character, shiftKey, altKey, ctrlKey, metaKey) === false ) 2690 2691 // cancelEvent( e ); // dismiss key 2692 2693 //} 2694 2695 2696 2697 // Keys can change the selection 2698 2699 if( popup_saved_selection ) 2700 2701 popup_saved_selection = saveSelection( node_contenteditable ); 2702 2703 if( phase == 2 || phase == 3 ) 2704 2705 { 2706 2707 if( debounced_handleSelection ) 2708 2709 debounced_handleSelection( null, null, false ); 2710 2711 } 2712 2713 // Most keys can cause text-changes 2714 2715 if( phase == 2 && debounced_syncTextarea ) 2716 2717 { 2718 2719 switch( key ) 2720 2721 { 2722 2723 case 33: // pageUp 2724 2725 case 34: // pageDown 2726 2727 case 35: // end 2728 2729 case 36: // home 2730 2731 case 37: // left 2732 2733 case 38: // up 2734 2735 case 39: // right 2736 2737 case 40: // down 2738 2739 // cursors do not 2740 2741 break; 2742 2743 default: 2744 2745 // call change handler 2746 2747 debounced_syncTextarea(); 2748 2749 break; 2750 2751 } 2752 2753 } 2754 2755 }; 2756 2757 addEvent( node_contenteditable, 'keydown', function( e ) 2758 2759 { 2760 2761 keyHandler( e, 1 ); 2762 2763 }); 2764 2765 addEvent( node_contenteditable, 'keypress', function( e ) 2766 2767 { 2768 2769 keyHandler( e, 2 ); 2770 2771 }); 2772 2773 addEvent( node_contenteditable, 'keyup', function( e ) 2774 2775 { 2776 2777 keyHandler( e, 3 ); 2778 2779 }); 2780 2781 2782 2783 // Mouse events 2784 2785 var mouseHandler = function( e, rightclick ) 2786 2787 { 2788 2789 // mouse position 2790 2791 var clientX = null, 2792 2793 clientY = null; 2794 2795 if( e.clientX && e.clientY ) 2796 2797 { 2798 2799 clientX = e.clientX; 2800 2801 clientY = e.clientY; 2802 2803 } 2804 2805 else if( e.pageX && e.pageY ) 2806 2807 { 2808 2809 clientX = e.pageX - window.pageXOffset; 2810 2811 clientY = e.pageY - window.pageYOffset; 2812 2813 } 2814 2815 // mouse button 2816 2817 if( e.which && e.which == 3 ) 2818 2819 rightclick = true; 2820 2821 else if( e.button && e.button == 2 ) 2822 2823 rightclick = true; 2824 2825 2826 2827 // remove event handler 2828 2829 removeEvent( window, 'mouseup', mouseHandler ); 2830 2831 // Callback selection 2832 2833 if( popup_saved_selection ) 2834 2835 popup_saved_selection = saveSelection( node_contenteditable ); 2836 2837 if( ! hijackContextmenu && rightclick ) 2838 2839 return; 2840 2841 if( debounced_handleSelection ) 2842 2843 debounced_handleSelection( clientX, clientY, rightclick ); 2844 2845 }; 2846 2847 var mouse_down_target = null; 2848 2849 addEvent( node_contenteditable, 'mousedown', function( e ) 2850 2851 { 2852 2853 // catch event if 'mouseup' outside 'contenteditable' 2854 2855 removeEvent( window, 'mouseup', mouseHandler ); 2856 2857 addEvent( window, 'mouseup', mouseHandler ); 2858 2859 // remember target 2860 2861 mouse_down_target = e.target; 2862 2863 }); 2864 2865 addEvent( node_contenteditable, 'mouseup', function( e ) 2866 2867 { 2868 2869 // Select image (improve user experience on Webkit) 2870 2871 var node = e.target; 2872 2873 if( node && node.nodeType == Node.ELEMENT_NODE && node === mouse_down_target && 2874 2875 isMediaNode(node) && isOrContainsNode(node_contenteditable, node, true) ) 2876 2877 { 2878 2879 var selection = window.getSelection(); 2880 2881 var range = document.createRange(); 2882 2883 range.setStartBefore( node ); 2884 2885 range.setEndAfter( node ); 2886 2887 selection.removeAllRanges(); 2888 2889 selection.addRange( range ); 2890 2891 } 2892 2893 // handle click 2894 2895 mouseHandler( e ); 2896 2897 // Trigger change 2898
2899 if( debounced_syncTextarea ) 2900 2901 debounced_syncTextarea(); 2902 2903 }); 2904 2905 addEvent( node_contenteditable, 'dblclick', function( e ) 2906 2907 { 2908 2909 mouseHandler( e ); 2910 2911 }); 2912 2913 addEvent( node_contenteditable, 'selectionchange', function( e ) 2914 2915 { 2916 2917 mouseHandler( e ); 2918 2919 }); 2920 2921 if( hijackContextmenu ) 2922 2923 { 2924 2925 addEvent( node_contenteditable, 'contextmenu', function( e ) 2926 2927 { 2928 2929 mouseHandler( e, true ); 2930 2931 cancelEvent( e ); 2932 2933 }); 2934 2935 } 2936 2937 2938 2939 // exec command 2940 2941 // https://developer.mozilla.org/en-US/docs/Web/API/document.execCommand 2942 2943 // http://www.quirksmode.org/dom/execCommand.html 2944 2945 var execCommand = function( command, param, force_selection ) 2946 2947 { 2948 2949 // give selection to contenteditable element 2950 2951 restoreSelection( node_contenteditable, popup_saved_selection ); 2952 2953 // tried to avoid forcing focus(), but ... - https://github.com/wysiwygjs/wysiwyg.js/issues/51 2954 2955 node_contenteditable.focus(); 2956 2957 if( ! selectionInside(node_contenteditable, force_selection) ) // returns 'selection inside editor' 2958 2959 return false; 2960 2961 2962 2963 // Buggy, call within 'try/catch' 2964 2965 try { 2966 2967 if( document.queryCommandSupported && ! document.queryCommandSupported(command) ) 2968 2969 return false; 2970 2971 return document.execCommand( command, false, param ); 2972 2973 } 2974 2975 catch( e ) { 2976 2977 } 2978 2979 return false; 2980 2981 }; 2982 2983 2984 2985 // copy/paste images from clipboard 2986 2987 addEvent( node_contenteditable, 'paste', function( e ) 2988 2989 { 2990 2991 var clipboardData = e.clipboardData; 2992 2993 if( ! clipboardData ) 2994 2995 return; 2996 2997 var items = clipboardData.items; 2998 2999 if( ! items || ! items.length ) 3000 3001 return; 3002 3003 var item = items[0]; 3004 3005 if( ! item.type.match(/^image\//) ) 3006 3007 return; 3008 3009 // Insert image from clipboard 3010 3011 filecontents( item.getAsFile(), function( type, dataurl ) { 3012 3013 if( dataurl ) // empty on error 3014 3015 execCommand( 'insertImage', dataurl ); 3016 3017 }); 3018 3019 cancelEvent( e ); // dismiss paste 3020 3021 }); 3022 3023 3024 3025 // Command structure 3026 3027 callUpdates = function( selection_destroyed ) 3028 3029 { 3030 3031 // change-handler 3032 3033 if( debounced_syncTextarea ) 3034 3035 debounced_syncTextarea(); 3036 3037 // handle saved selection 3038 3039 if( selection_destroyed ) 3040 3041 { 3042 3043 collapseSelectionEnd(); 3044 3045 popup_saved_selection = null; // selection destroyed 3046 3047 } 3048 3049 else if( popup_saved_selection ) 3050 3051 popup_saved_selection = saveSelection( node_contenteditable ); 3052 3053 }; 3054 3055 commands = { 3056 3057 // properties 3058 3059 sync: function() 3060 3061 { 3062 3063 // sync textarea immediately 3064 3065 if( syncTextarea ) 3066 3067 syncTextarea(); 3068 3069 return this; 3070 3071 }, 3072 3073 getHTML: function() 3074 3075 { 3076 3077 return node_contenteditable.innerHTML; 3078 3079 }, 3080 3081 setHTML: function( html ) 3082 3083 { 3084 3085 node_contenteditable.innerHTML = html || ''; 3086 3087 callUpdates( true ); // selection destroyed 3088 3089 return this; 3090 3091 }, 3092 3093 getSelectedHTML: function() 3094 3095 { 3096 3097 restoreSelection( node_contenteditable, popup_saved_selection ); 3098 3099 if( ! selectionInside(node_contenteditable) ) 3100 3101 return null; 3102 3103 return getSelectionHtml( node_contenteditable ); 3104 3105 }, 3106 3107 // selection and popup 3108 3109 collapseSelection: function() 3110 3111 { 3112 3113 collapseSelectionEnd(); 3114 3115 popup_saved_selection = null; // selection destroyed 3116 3117 return this; 3118 3119 }, 3120 3121 expandSelection: function( preceding, following ) 3122 3123 { 3124 3125 restoreSelection( node_contenteditable, popup_saved_selection ); 3126 3127 if( ! selectionInside(node_contenteditable) ) 3128 3129 return this; 3130 3131 expandSelectionCaret( node_contenteditable, preceding, following ); 3132 3133 popup_saved_selection = saveSelection( node_contenteditable ); // save new selection 3134 3135 return this; 3136 3137 }, 3138 3139 openPopup: function() 3140 3141 { 3142 3143 if( ! popup_saved_selection ) 3144 3145 popup_saved_selection = saveSelection( node_contenteditable ); // save current selection 3146 3147 return popupOpen(); 3148 3149 }, 3150 3151 activePopup: function() 3152 3153 { 3154 3155 return node_popup; 3156 3157 }, 3158 3159 closePopup: function() 3160 3161 { 3162 3163 popupClose(); 3164 3165 return this; 3166 3167 }, 3168 3169 // formats 3170 3171 removeFormat: function() 3172 3173 { 3174 3175 execCommand( 'removeFormat' ); 3176 3177 execCommand( 'unlink' ); 3178 3179 callUpdates(); 3180 3181 return this; 3182 3183 }, 3184 3185 bold: function() 3186 3187 { 3188 3189 execCommand( 'bold' ); 3190 3191 callUpdates(); 3192 3193 return this; 3194 3195 }, 3196 3197 italic: function() 3198 3199 { 3200 3201 execCommand( 'italic' ); 3202 3203 callUpdates(); 3204 3205 return this; 3206 3207 }, 3208 3209 underline: function() 3210 3211 { 3212 3213 execCommand( 'underline' ); 3214 3215 callUpdates(); 3216 3217 return this; 3218 3219 }, 3220
3221 strikethrough: function() 3222 3223 { 3224 3225 execCommand( 'strikeThrough' ); 3226 3227 callUpdates(); 3228 3229 return this; 3230 3231 }, 3232 3233 forecolor: function( color ) 3234 3235 { 3236 3237 execCommand( 'foreColor', color ); 3238 3239 callUpdates(); 3240 3241 return this; 3242 3243 }, 3244 3245 highlight: function( color ) 3246 3247 { 3248 3249 // http://stackoverflow.com/questions/2756931/highlight-the-text-of-the-dom-range-element 3250 3251 if( ! execCommand('hiliteColor',color) ) // some browsers apply 'backColor' to the whole block 3252 3253 execCommand( 'backColor', color ); 3254 3255 callUpdates(); 3256 3257 return this; 3258 3259 }, 3260 3261 fontName: function( name ) 3262 3263 { 3264 3265 execCommand( 'fontName', name ); 3266 3267 callUpdates(); 3268 3269 return this; 3270 3271 }, 3272 3273 fontSize: function( size ) 3274 3275 { 3276 3277 execCommand( 'fontSize', size ); 3278 3279 callUpdates(); 3280 3281 return this; 3282 3283 }, 3284 3285 subscript: function() 3286 3287 { 3288 3289 execCommand( 'subscript' ); 3290 3291 callUpdates(); 3292 3293 return this; 3294 3295 }, 3296 3297 superscript: function() 3298 3299 { 3300 3301 execCommand( 'superscript' ); 3302 3303 callUpdates(); 3304 3305 return this; 3306 3307 }, 3308 3309 insertLink: function( url ) 3310 3311 { 3312 3313 execCommand( 'createLink', url ); 3314 3315 callUpdates( true ); // selection destroyed 3316 3317 return this; 3318 3319 }, 3320 3321 insertImage: function( url ) 3322 3323 { 3324 3325 execCommand( 'insertImage', url, true ); 3326 3327 callUpdates( true ); // selection destroyed 3328 3329 return this; 3330 3331 }, 3332 3333 insertHTML: function( html ) 3334 3335 { 3336 3337 if( ! execCommand('insertHTML', html, true) ) 3338 3339 { 3340 3341 // IE 11 still does not support 'insertHTML' 3342 3343 restoreSelection( node_contenteditable, popup_saved_selection ); 3344 3345 selectionInside( node_contenteditable, true ); 3346 3347 pasteHtmlAtCaret( node_contenteditable, html ); 3348 3349 } 3350 3351 callUpdates( true ); // selection destroyed 3352 3353 return this; 3354 3355 }, 3356 3357 orderedList: function() 3358 3359 { 3360 3361 execCommand( 'insertOrderedList' ); 3362 3363 callUpdates(); 3364 3365 return this; 3366 3367 }, 3368 3369 unorderedList: function() 3370 3371 { 3372 3373 execCommand( 'insertUnorderedList' ); 3374 3375 callUpdates(); 3376 3377 return this; 3378 3379 }, 3380 3381 }; 3382 3383 3384 3385 // Create toolbar 3386 3387 if( buttons ) 3388 3389 { 3390 3391 var toolbar_element = document.createElement('div'); 3392 3393 add_class( toolbar_element, 'toolbar' ); 3394 3395 if( toolbar_top ) 3396 3397 { 3398 3399 add_class( toolbar_element, 'toolbar-top' ); 3400 3401 node_container.insertBefore( toolbar_element, node_container.firstChild ); 3402 3403 } 3404 3405 else if( toolbar_bottom ) 3406 3407 { 3408 3409 add_class( toolbar_element, 'toolbar-bottom' ); 3410 3411 node_container.appendChild( toolbar_element ); 3412 3413 } 3414 3415 else 3416 3417 { 3418 3419 var toolbar_wrapper = document.createElement('div'); 3420 3421 add_class( toolbar_wrapper, 'toolbar-auto' ); 3422 3423 node_container.appendChild( toolbar_wrapper ); 3424 3425 toolbar_wrapper.appendChild( toolbar_element ); 3426 3427 } 3428 3429 fill_buttons( toolbar_element, null, buttons, hotkeys ); 3430 3431 } 3432 3433 3434 3435 return commands; 3436 3437 }; 3438 3439 3440 3441 // https://www.matteoagosti.com/blog/2013/02/24/writing-javascript-modules-for-both-browser-and-node/ 3442 3443 if( typeof module !== 'undefined' && typeof module.exports !== 'undefined' ) 3444 3445 module.exports = wysiwyg; 3446 3447 else 3448 3449 window.wysiwyg = wysiwyg; 3450 3451})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.