github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/pubnot/trumbowyg/trumbowyg.js (about) 1 /** 2 * Trumbowyg v2.8.1 - A lightweight WYSIWYG editor 3 * Trumbowyg core file 4 * ------------------------ 5 * @link http://alex-d.github.io/Trumbowyg 6 * @license MIT 7 * @author Alexandre Demode (Alex-D) 8 * Twitter : @AlexandreDemode 9 * Website : alex-d.fr 10 */ 11 12 jQuery.trumbowyg = { 13 langs: { 14 en: { 15 viewHTML: 'View HTML', 16 17 undo: 'Undo', 18 redo: 'Redo', 19 20 formatting: 'Formatting', 21 p: 'Paragraph', 22 blockquote: 'Quote', 23 code: 'Code', 24 header: 'Header', 25 26 bold: 'Bold', 27 italic: 'Italic', 28 strikethrough: 'Stroke', 29 underline: 'Underline', 30 31 strong: 'Strong', 32 em: 'Emphasis', 33 del: 'Deleted', 34 35 superscript: 'Superscript', 36 subscript: 'Subscript', 37 38 unorderedList: 'Unordered list', 39 orderedList: 'Ordered list', 40 41 insertImage: 'Insert Image', 42 link: 'Link', 43 createLink: 'Insert link', 44 unlink: 'Remove link', 45 46 justifyLeft: 'Align Left', 47 justifyCenter: 'Align Center', 48 justifyRight: 'Align Right', 49 justifyFull: 'Align Justify', 50 51 horizontalRule: 'Insert horizontal rule', 52 removeformat: 'Remove format', 53 54 fullscreen: 'Fullscreen', 55 56 close: 'Close', 57 58 submit: 'Confirm', 59 reset: 'Cancel', 60 61 required: 'Required', 62 description: 'Description', 63 title: 'Title', 64 text: 'Text', 65 target: 'Target' 66 } 67 }, 68 69 // Plugins 70 plugins: {}, 71 72 // SVG Path globally 73 svgPath: null, 74 75 hideButtonTexts: null 76 }; 77 78 // Makes default options read-only 79 Object.defineProperty(jQuery.trumbowyg, 'defaultOptions', { 80 value: { 81 lang: 'en', 82 83 fixedBtnPane: false, 84 fixedFullWidth: false, 85 autogrow: false, 86 autogrowOnEnter: false, 87 88 prefix: 'trumbowyg-', 89 90 semantic: true, 91 resetCss: false, 92 removeformatPasted: false, 93 tagsToRemove: [], 94 btns: [ 95 ['viewHTML'], 96 ['undo', 'redo'], // Only supported in Blink browsers 97 ['formatting'], 98 ['strong', 'em', 'del'], 99 ['superscript', 'subscript'], 100 ['link'], 101 ['insertImage'], 102 ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'], 103 ['unorderedList', 'orderedList'], 104 ['horizontalRule'], 105 ['removeformat'], 106 ['fullscreen'] 107 ], 108 // For custom button definitions 109 btnsDef: {}, 110 111 inlineElementsSelector: 'a,abbr,acronym,b,caption,cite,code,col,dfn,dir,dt,dd,em,font,hr,i,kbd,li,q,span,strikeout,strong,sub,sup,u', 112 113 pasteHandlers: [], 114 115 // imgDblClickHandler: default is defined in constructor 116 117 plugins: {} 118 }, 119 writable: false, 120 enumerable: true, 121 configurable: false 122 }); 123 124 125 (function (navigator, window, document, $) { 126 'use strict'; 127 128 var CONFIRM_EVENT = 'tbwconfirm', 129 CANCEL_EVENT = 'tbwcancel'; 130 131 $.fn.trumbowyg = function (options, params) { 132 var trumbowygDataName = 'trumbowyg'; 133 if (options === Object(options) || !options) { 134 return this.each(function () { 135 if (!$(this).data(trumbowygDataName)) { 136 $(this).data(trumbowygDataName, new Trumbowyg(this, options)); 137 } 138 }); 139 } 140 if (this.length === 1) { 141 try { 142 var t = $(this).data(trumbowygDataName); 143 switch (options) { 144 // Exec command 145 case 'execCmd': 146 return t.execCmd(params.cmd, params.param, params.forceCss); 147 148 // Modal box 149 case 'openModal': 150 return t.openModal(params.title, params.content); 151 case 'closeModal': 152 return t.closeModal(); 153 case 'openModalInsert': 154 return t.openModalInsert(params.title, params.fields, params.callback); 155 156 // Range 157 case 'saveRange': 158 return t.saveRange(); 159 case 'getRange': 160 return t.range; 161 case 'getRangeText': 162 return t.getRangeText(); 163 case 'restoreRange': 164 return t.restoreRange(); 165 166 // Enable/disable 167 case 'enable': 168 return t.setDisabled(false); 169 case 'disable': 170 return t.setDisabled(true); 171 172 // Destroy 173 case 'destroy': 174 return t.destroy(); 175 176 // Empty 177 case 'empty': 178 return t.empty(); 179 180 // HTML 181 case 'html': 182 return t.html(params); 183 } 184 } catch (c) { 185 } 186 } 187 188 return false; 189 }; 190 191 // @param: editorElem is the DOM element 192 var Trumbowyg = function (editorElem, options) { 193 var t = this, 194 trumbowygIconsId = 'trumbowyg-icons', 195 $trumbowyg = $.trumbowyg; 196 197 // Get the document of the element. It use to makes the plugin 198 // compatible on iframes. 199 t.doc = editorElem.ownerDocument || document; 200 201 // jQuery object of the editor 202 t.$ta = $(editorElem); // $ta : Textarea 203 t.$c = $(editorElem); // $c : creator 204 205 options = options || {}; 206 207 // Localization management 208 if (options.lang != null || $trumbowyg.langs[options.lang] != null) { 209 t.lang = $.extend(true, {}, $trumbowyg.langs.en, $trumbowyg.langs[options.lang]); 210 } else { 211 t.lang = $trumbowyg.langs.en; 212 } 213 214 t.hideButtonTexts = $trumbowyg.hideButtonTexts != null ? $trumbowyg.hideButtonTexts : options.hideButtonTexts; 215 216 // SVG path 217 var svgPathOption = $trumbowyg.svgPath != null ? $trumbowyg.svgPath : options.svgPath; 218 t.hasSvg = svgPathOption !== false; 219 t.svgPath = !!t.doc.querySelector('base') ? window.location.href.split('#')[0] : ''; 220 if ($('#' + trumbowygIconsId, t.doc).length === 0 && svgPathOption !== false) { 221 if (svgPathOption == null) { 222 // Hack to get svgPathOption based on trumbowyg.js path 223 try { 224 throw new Error(); 225 } catch (e) { 226 if (!e.hasOwnProperty('stack')) { 227 console.warn('You must define svgPath: https://goo.gl/CfTY9U'); // jshint ignore:line 228 } else { 229 var stackLines = e.stack.split('\n'); 230 231 for (var i in stackLines) { 232 if (!stackLines[i].match(/https?:\/\//)) { 233 continue; 234 } 235 svgPathOption = stackLines[Number(i)].match(/((https?:\/\/.+\/)([^\/]+\.js))(\?.*)?:/)[1].split('/'); 236 svgPathOption.pop(); 237 svgPathOption = svgPathOption.join('/') + '/ui/icons.svg'; 238 break; 239 } 240 } 241 } 242 } 243 244 var div = t.doc.createElement('div'); 245 div.id = trumbowygIconsId; 246 t.doc.body.insertBefore(div, t.doc.body.childNodes[0]); 247 $.ajax({ 248 async: true, 249 type: 'GET', 250 contentType: 'application/x-www-form-urlencoded; charset=UTF-8', 251 dataType: 'xml', 252 crossDomain: true, 253 url: svgPathOption, 254 data: null, 255 beforeSend: null, 256 complete: null, 257 success: function (data) { 258 div.innerHTML = new XMLSerializer().serializeToString(data.documentElement); 259 } 260 }); 261 } 262 263 264 /** 265 * When the button is associated to a empty object 266 * fn and title attributs are defined from the button key value 267 * 268 * For example 269 * foo: {} 270 * is equivalent to : 271 * foo: { 272 * fn: 'foo', 273 * title: this.lang.foo 274 * } 275 */ 276 var h = t.lang.header, // Header translation 277 isBlinkFunction = function () { 278 return (window.chrome || (window.Intl && Intl.v8BreakIterator)) && 'CSS' in window; 279 }; 280 t.btnsDef = { 281 viewHTML: { 282 fn: 'toggle' 283 }, 284 285 undo: { 286 isSupported: isBlinkFunction, 287 key: 'Z' 288 }, 289 redo: { 290 isSupported: isBlinkFunction, 291 key: 'Y' 292 }, 293 294 p: { 295 fn: 'formatBlock' 296 }, 297 blockquote: { 298 fn: 'formatBlock' 299 }, 300 h1: { 301 fn: 'formatBlock', 302 title: h + ' 1' 303 }, 304 h2: { 305 fn: 'formatBlock', 306 title: h + ' 2' 307 }, 308 h3: { 309 fn: 'formatBlock', 310 title: h + ' 3' 311 }, 312 h4: { 313 fn: 'formatBlock', 314 title: h + ' 4' 315 }, 316 subscript: { 317 tag: 'sub' 318 }, 319 superscript: { 320 tag: 'sup' 321 }, 322 323 bold: { 324 key: 'B', 325 tag: 'b' 326 }, 327 italic: { 328 key: 'I', 329 tag: 'i' 330 }, 331 underline: { 332 tag: 'u' 333 }, 334 strikethrough: { 335 tag: 'strike' 336 }, 337 338 strong: { 339 fn: 'bold', 340 key: 'B' 341 }, 342 em: { 343 fn: 'italic', 344 key: 'I' 345 }, 346 del: { 347 fn: 'strikethrough' 348 }, 349 350 createLink: { 351 key: 'K', 352 tag: 'a' 353 }, 354 unlink: {}, 355 356 insertImage: {}, 357 358 justifyLeft: { 359 tag: 'left', 360 forceCss: true 361 }, 362 justifyCenter: { 363 tag: 'center', 364 forceCss: true 365 }, 366 justifyRight: { 367 tag: 'right', 368 forceCss: true 369 }, 370 justifyFull: { 371 tag: 'justify', 372 forceCss: true 373 }, 374 375 unorderedList: { 376 fn: 'insertUnorderedList', 377 tag: 'ul' 378 }, 379 orderedList: { 380 fn: 'insertOrderedList', 381 tag: 'ol' 382 }, 383 384 horizontalRule: { 385 fn: 'insertHorizontalRule' 386 }, 387 388 removeformat: {}, 389 390 fullscreen: { 391 class: 'trumbowyg-not-disable' 392 }, 393 close: { 394 fn: 'destroy', 395 class: 'trumbowyg-not-disable' 396 }, 397 398 // Dropdowns 399 formatting: { 400 dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4'], 401 ico: 'p' 402 }, 403 link: { 404 dropdown: ['createLink', 'unlink'] 405 } 406 }; 407 408 // Defaults Options 409 t.o = $.extend(true, {}, $trumbowyg.defaultOptions, options); 410 if (!t.o.hasOwnProperty('imgDblClickHandler')) { 411 t.o.imgDblClickHandler = t.getDefaultImgDblClickHandler(); 412 } 413 414 t.disabled = t.o.disabled || (editorElem.nodeName === 'TEXTAREA' && editorElem.disabled); 415 416 if (options.btns) { 417 t.o.btns = options.btns; 418 } else if (!t.o.semantic) { 419 t.o.btns[3] = ['bold', 'italic', 'underline', 'strikethrough']; 420 } 421 422 $.each(t.o.btnsDef, function (btnName, btnDef) { 423 t.addBtnDef(btnName, btnDef); 424 }); 425 426 // put this here in the event it would be merged in with options 427 t.eventNamespace = 'trumbowyg-event'; 428 429 // Keyboard shortcuts are load in this array 430 t.keys = []; 431 432 // Tag to button dynamically hydrated 433 t.tagToButton = {}; 434 t.tagHandlers = []; 435 436 // Admit multiple paste handlers 437 t.pasteHandlers = [].concat(t.o.pasteHandlers); 438 439 // Check if browser is IE 440 t.isIE = (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') !== -1); 441 442 t.init(); 443 }; 444 445 Trumbowyg.prototype = { 446 init: function () { 447 var t = this; 448 t.height = t.$ta.height(); 449 450 t.initPlugins(); 451 452 try { 453 // Disable image resize, try-catch for old IE 454 t.doc.execCommand('enableObjectResizing', false, false); 455 t.doc.execCommand('defaultParagraphSeparator', false, 'p'); 456 } catch (e) { 457 } 458 459 t.buildEditor(); 460 t.buildBtnPane(); 461 462 t.fixedBtnPaneEvents(); 463 464 t.buildOverlay(); 465 466 setTimeout(function () { 467 if (t.disabled) { 468 t.setDisabled(true); 469 } 470 t.$c.trigger('tbwinit'); 471 }); 472 }, 473 474 addBtnDef: function (btnName, btnDef) { 475 this.btnsDef[btnName] = btnDef; 476 }, 477 478 buildEditor: function () { 479 var t = this, 480 prefix = t.o.prefix, 481 html = ''; 482 483 t.$box = $('<div/>', { 484 class: prefix + 'box ' + prefix + 'editor-visible ' + prefix + t.o.lang + ' trumbowyg' 485 }); 486 487 // $ta = Textarea 488 // $ed = Editor 489 t.isTextarea = t.$ta.is('textarea'); 490 if (t.isTextarea) { 491 html = t.$ta.val(); 492 t.$ed = $('<div/>'); 493 t.$box 494 .insertAfter(t.$ta) 495 .append(t.$ed, t.$ta); 496 } else { 497 t.$ed = t.$ta; 498 html = t.$ed.html(); 499 500 t.$ta = $('<textarea/>', { 501 name: t.$ta.attr('id'), 502 height: t.height 503 }).val(html); 504 505 t.$box 506 .insertAfter(t.$ed) 507 .append(t.$ta, t.$ed); 508 t.syncCode(); 509 } 510 511 t.$ta 512 .addClass(prefix + 'textarea') 513 .attr('tabindex', -1) 514 ; 515 516 t.$ed 517 .addClass(prefix + 'editor') 518 .attr({ 519 contenteditable: true, 520 dir: t.lang._dir || 'ltr' 521 }) 522 .html(html) 523 ; 524 525 if (t.o.tabindex) { 526 t.$ed.attr('tabindex', t.o.tabindex); 527 } 528 529 if (t.$c.is('[placeholder]')) { 530 t.$ed.attr('placeholder', t.$c.attr('placeholder')); 531 } 532 533 if (t.$c.is('[spellcheck]')) { 534 t.$ed.attr('spellcheck', t.$c.attr('spellcheck')); 535 } 536 537 if (t.o.resetCss) { 538 t.$ed.addClass(prefix + 'reset-css'); 539 } 540 541 if (!t.o.autogrow) { 542 t.$ta.add(t.$ed).css({ 543 height: t.height 544 }); 545 } 546 547 t.semanticCode(); 548 549 if (t.o.autogrowOnEnter) { 550 t.$ed.addClass(prefix + 'autogrow-on-enter'); 551 } 552 553 var ctrl = false, 554 composition = false, 555 debounceButtonPaneStatus, 556 updateEventName = t.isIE ? 'keyup' : 'input'; 557 558 t.$ed 559 .on('dblclick', 'img', t.o.imgDblClickHandler) 560 .on('keydown', function (e) { 561 if ((e.ctrlKey || e.metaKey) && !e.altKey) { 562 ctrl = true; 563 var key = t.keys[String.fromCharCode(e.which).toUpperCase()]; 564 565 try { 566 t.execCmd(key.fn, key.param); 567 return false; 568 } catch (c) { 569 } 570 } 571 }) 572 .on('compositionstart compositionupdate', function () { 573 composition = true; 574 }) 575 .on(updateEventName + ' compositionend', function (e) { 576 if (e.type === 'compositionend') { 577 composition = false; 578 } else if (composition) { 579 return; 580 } 581 582 var keyCode = e.which; 583 584 if (keyCode >= 37 && keyCode <= 40) { 585 return; 586 } 587 588 if ((e.ctrlKey || e.metaKey) && (keyCode === 89 || keyCode === 90)) { 589 t.$c.trigger('tbwchange'); 590 } else if (!ctrl && keyCode !== 17) { 591 t.semanticCode(false, e.type === 'compositionend' && keyCode === 13); 592 t.$c.trigger('tbwchange'); 593 } else if (typeof e.which === 'undefined') { 594 t.semanticCode(false, false, true); 595 } 596 597 setTimeout(function () { 598 ctrl = false; 599 }, 200); 600 }) 601 .on('mouseup keydown keyup', function () { 602 clearTimeout(debounceButtonPaneStatus); 603 debounceButtonPaneStatus = setTimeout(function () { 604 t.updateButtonPaneStatus(); 605 }, 50); 606 }) 607 .on('focus blur', function (e) { 608 t.$c.trigger('tbw' + e.type); 609 if (e.type === 'blur') { 610 $('.' + prefix + 'active-button', t.$btnPane).removeClass(prefix + 'active-button ' + prefix + 'active'); 611 } 612 if (t.o.autogrowOnEnter) { 613 if (t.autogrowOnEnterDontClose) { 614 return; 615 } 616 if (e.type === 'focus') { 617 t.autogrowOnEnterWasFocused = true; 618 t.autogrowEditorOnEnter(); 619 } 620 else if (!t.o.autogrow) { 621 t.$ed.css({height: t.$ed.css('min-height')}); 622 t.$c.trigger('tbwresize'); 623 } 624 } 625 }) 626 .on('cut', function () { 627 setTimeout(function () { 628 t.semanticCode(false, true); 629 t.$c.trigger('tbwchange'); 630 }, 0); 631 }) 632 .on('paste', function (e) { 633 if (t.o.removeformatPasted) { 634 e.preventDefault(); 635 636 if (window.getSelection && window.getSelection().deleteFromDocument) { 637 window.getSelection().deleteFromDocument(); 638 } 639 640 try { 641 // IE 642 var text = window.clipboardData.getData('Text'); 643 644 try { 645 // <= IE10 646 t.doc.selection.createRange().pasteHTML(text); 647 } catch (c) { 648 // IE 11 649 t.doc.getSelection().getRangeAt(0).insertNode(t.doc.createTextNode(text)); 650 } 651 t.$c.trigger('tbwchange', e); 652 } catch (d) { 653 // Not IE 654 t.execCmd('insertText', (e.originalEvent || e).clipboardData.getData('text/plain')); 655 } 656 } 657 658 // Call pasteHandlers 659 $.each(t.pasteHandlers, function (i, pasteHandler) { 660 pasteHandler(e); 661 }); 662 663 setTimeout(function () { 664 t.semanticCode(false, true); 665 t.$c.trigger('tbwpaste', e); 666 }, 0); 667 }); 668 669 t.$ta 670 .on('keyup', function () { 671 t.$c.trigger('tbwchange'); 672 }) 673 .on('paste', function () { 674 setTimeout(function () { 675 t.$c.trigger('tbwchange'); 676 }, 0); 677 }); 678 679 t.$box.on('keydown', function (e) { 680 if (e.which === 27 && $('.' + prefix + 'modal-box', t.$box).length === 1) { 681 t.closeModal(); 682 return false; 683 } 684 }); 685 }, 686 687 //autogrow when entering logic 688 autogrowEditorOnEnter: function () { 689 var t = this; 690 t.$ed.removeClass('autogrow-on-enter'); 691 var oldHeight = t.$ed[0].clientHeight; 692 t.$ed.height('auto'); 693 var totalHeight = t.$ed[0].scrollHeight; 694 t.$ed.addClass('autogrow-on-enter'); 695 if (oldHeight !== totalHeight) { 696 t.$ed.height(oldHeight); 697 setTimeout(function () { 698 t.$ed.css({height: totalHeight}); 699 t.$c.trigger('tbwresize'); 700 }, 0); 701 } 702 }, 703 704 705 // Build button pane, use o.btns option 706 buildBtnPane: function () { 707 var t = this, 708 prefix = t.o.prefix; 709 710 var $btnPane = t.$btnPane = $('<div/>', { 711 class: prefix + 'button-pane' 712 }); 713 714 $.each(t.o.btns, function (i, btnGrp) { 715 if (!$.isArray(btnGrp)) { 716 btnGrp = [btnGrp]; 717 } 718 719 var $btnGroup = $('<div/>', { 720 class: prefix + 'button-group ' + ((btnGrp.indexOf('fullscreen') >= 0) ? prefix + 'right' : '') 721 }); 722 $.each(btnGrp, function (i, btn) { 723 try { // Prevent buildBtn error 724 if (t.isSupportedBtn(btn)) { // It's a supported button 725 $btnGroup.append(t.buildBtn(btn)); 726 } 727 } catch (c) { 728 } 729 }); 730 $btnPane.append($btnGroup); 731 }); 732 733 t.$box.prepend($btnPane); 734 }, 735 736 737 // Build a button and his action 738 buildBtn: function (btnName) { // btnName is name of the button 739 var t = this, 740 prefix = t.o.prefix, 741 btn = t.btnsDef[btnName], 742 isDropdown = btn.dropdown, 743 hasIcon = btn.hasIcon != null ? btn.hasIcon : true, 744 textDef = t.lang[btnName] || btnName, 745 746 $btn = $('<button/>', { 747 type: 'button', 748 class: prefix + btnName + '-button ' + (btn.class || '') + (!hasIcon ? ' ' + prefix + 'textual-button' : ''), 749 html: t.hasSvg && hasIcon ? 750 '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' : 751 t.hideButtonTexts ? '' : (btn.text || btn.title || t.lang[btnName] || btnName), 752 title: (btn.title || btn.text || textDef) + ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : ''), 753 tabindex: -1, 754 mousedown: function () { 755 if (!isDropdown || $('.' + btnName + '-' + prefix + 'dropdown', t.$box).is(':hidden')) { 756 $('body', t.doc).trigger('mousedown'); 757 } 758 759 if (t.$btnPane.hasClass(prefix + 'disable') && !$(this).hasClass(prefix + 'active') && !$(this).hasClass(prefix + 'not-disable')) { 760 return false; 761 } 762 763 t.execCmd((isDropdown ? 'dropdown' : false) || btn.fn || btnName, btn.param || btnName, btn.forceCss); 764 765 return false; 766 } 767 }); 768 769 if (isDropdown) { 770 $btn.addClass(prefix + 'open-dropdown'); 771 var dropdownPrefix = prefix + 'dropdown', 772 $dropdown = $('<div/>', { // the dropdown 773 class: dropdownPrefix + '-' + btnName + ' ' + dropdownPrefix + ' ' + prefix + 'fixed-top', 774 'data-dropdown': btnName 775 }); 776 $.each(isDropdown, function (i, def) { 777 if (t.btnsDef[def] && t.isSupportedBtn(def)) { 778 $dropdown.append(t.buildSubBtn(def)); 779 } 780 }); 781 t.$box.append($dropdown.hide()); 782 } else if (btn.key) { 783 t.keys[btn.key] = { 784 fn: btn.fn || btnName, 785 param: btn.param || btnName 786 }; 787 } 788 789 if (!isDropdown) { 790 t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName; 791 } 792 793 return $btn; 794 }, 795 // Build a button for dropdown menu 796 // @param n : name of the subbutton 797 buildSubBtn: function (btnName) { 798 var t = this, 799 prefix = t.o.prefix, 800 btn = t.btnsDef[btnName], 801 hasIcon = btn.hasIcon != null ? btn.hasIcon : true; 802 803 if (btn.key) { 804 t.keys[btn.key] = { 805 fn: btn.fn || btnName, 806 param: btn.param || btnName 807 }; 808 } 809 810 t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName; 811 812 return $('<button/>', { 813 type: 'button', 814 class: prefix + btnName + '-dropdown-button' + (btn.ico ? ' ' + prefix + btn.ico + '-button' : ''), 815 html: t.hasSvg && hasIcon ? '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' + (btn.text || btn.title || t.lang[btnName] || btnName) : (btn.text || btn.title || t.lang[btnName] || btnName), 816 title: ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : null), 817 style: btn.style || null, 818 mousedown: function () { 819 $('body', t.doc).trigger('mousedown'); 820 821 t.execCmd(btn.fn || btnName, btn.param || btnName, btn.forceCss); 822 823 return false; 824 } 825 }); 826 }, 827 // Check if button is supported 828 isSupportedBtn: function (b) { 829 try { 830 return this.btnsDef[b].isSupported(); 831 } catch (c) { 832 } 833 return true; 834 }, 835 836 // Build overlay for modal box 837 buildOverlay: function () { 838 var t = this; 839 t.$overlay = $('<div/>', { 840 class: t.o.prefix + 'overlay' 841 }).appendTo(t.$box); 842 return t.$overlay; 843 }, 844 showOverlay: function () { 845 var t = this; 846 $(window).trigger('scroll'); 847 t.$overlay.fadeIn(200); 848 t.$box.addClass(t.o.prefix + 'box-blur'); 849 }, 850 hideOverlay: function () { 851 var t = this; 852 t.$overlay.fadeOut(50); 853 t.$box.removeClass(t.o.prefix + 'box-blur'); 854 }, 855 856 // Management of fixed button pane 857 fixedBtnPaneEvents: function () { 858 var t = this, 859 fixedFullWidth = t.o.fixedFullWidth, 860 $box = t.$box; 861 862 if (!t.o.fixedBtnPane) { 863 return; 864 } 865 866 t.isFixed = false; 867 868 $(window) 869 .on('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace, function () { 870 if (!$box) { 871 return; 872 } 873 874 t.syncCode(); 875 876 var scrollTop = $(window).scrollTop(), 877 offset = $box.offset().top + 1, 878 bp = t.$btnPane, 879 oh = bp.outerHeight() - 2; 880 881 if ((scrollTop - offset > 0) && ((scrollTop - offset - t.height) < 0)) { 882 if (!t.isFixed) { 883 t.isFixed = true; 884 bp.css({ 885 position: 'fixed', 886 top: 0, 887 left: fixedFullWidth ? '0' : 'auto', 888 zIndex: 7 889 }); 890 $([t.$ta, t.$ed]).css({marginTop: bp.height()}); 891 } 892 bp.css({ 893 width: fixedFullWidth ? '100%' : (($box.width() - 1) + 'px') 894 }); 895 896 $('.' + t.o.prefix + 'fixed-top', $box).css({ 897 position: fixedFullWidth ? 'fixed' : 'absolute', 898 top: fixedFullWidth ? oh : oh + (scrollTop - offset) + 'px', 899 zIndex: 15 900 }); 901 } else if (t.isFixed) { 902 t.isFixed = false; 903 bp.removeAttr('style'); 904 $([t.$ta, t.$ed]).css({marginTop: 0}); 905 $('.' + t.o.prefix + 'fixed-top', $box).css({ 906 position: 'absolute', 907 top: oh 908 }); 909 } 910 }); 911 }, 912 913 // Disable editor 914 setDisabled: function (disable) { 915 var t = this, 916 prefix = t.o.prefix; 917 918 t.disabled = disable; 919 920 if (disable) { 921 t.$ta.attr('disabled', true); 922 } else { 923 t.$ta.removeAttr('disabled'); 924 } 925 t.$box.toggleClass(prefix + 'disabled', disable); 926 t.$ed.attr('contenteditable', !disable); 927 }, 928 929 // Destroy the editor 930 destroy: function () { 931 var t = this, 932 prefix = t.o.prefix; 933 934 if (t.isTextarea) { 935 t.$box.after( 936 t.$ta 937 .css({height: ''}) 938 .val(t.html()) 939 .removeClass(prefix + 'textarea') 940 .show() 941 ); 942 } else { 943 t.$box.after( 944 t.$ed 945 .css({height: ''}) 946 .removeClass(prefix + 'editor') 947 .removeAttr('contenteditable') 948 .removeAttr('dir') 949 .html(t.html()) 950 .show() 951 ); 952 } 953 954 t.$ed.off('dblclick', 'img'); 955 956 t.destroyPlugins(); 957 958 t.$box.remove(); 959 t.$c.removeData('trumbowyg'); 960 $('body').removeClass(prefix + 'body-fullscreen'); 961 t.$c.trigger('tbwclose'); 962 $(window).off('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace); 963 }, 964 965 966 // Empty the editor 967 empty: function () { 968 this.$ta.val(''); 969 this.syncCode(true); 970 }, 971 972 973 // Function call when click on viewHTML button 974 toggle: function () { 975 var t = this, 976 prefix = t.o.prefix; 977 978 if (t.o.autogrowOnEnter) { 979 t.autogrowOnEnterDontClose = !t.$box.hasClass(prefix + 'editor-hidden'); 980 } 981 982 t.semanticCode(false, true); 983 984 setTimeout(function () { 985 t.doc.activeElement.blur(); 986 t.$box.toggleClass(prefix + 'editor-hidden ' + prefix + 'editor-visible'); 987 t.$btnPane.toggleClass(prefix + 'disable'); 988 $('.' + prefix + 'viewHTML-button', t.$btnPane).toggleClass(prefix + 'active'); 989 if (t.$box.hasClass(prefix + 'editor-visible')) { 990 t.$ta.attr('tabindex', -1); 991 } else { 992 t.$ta.removeAttr('tabindex'); 993 } 994 995 if (t.o.autogrowOnEnter && !t.autogrowOnEnterDontClose) { 996 t.autogrowEditorOnEnter(); 997 } 998 }, 0); 999 }, 1000 1001 // Open dropdown when click on a button which open that 1002 dropdown: function (name) { 1003 var t = this, 1004 d = t.doc, 1005 prefix = t.o.prefix, 1006 $dropdown = $('[data-dropdown=' + name + ']', t.$box), 1007 $btn = $('.' + prefix + name + '-button', t.$btnPane), 1008 show = $dropdown.is(':hidden'); 1009 1010 $('body', d).trigger('mousedown'); 1011 1012 if (show) { 1013 var o = $btn.offset().left; 1014 $btn.addClass(prefix + 'active'); 1015 1016 $dropdown.css({ 1017 position: 'absolute', 1018 top: $btn.offset().top - t.$btnPane.offset().top + $btn.outerHeight(), 1019 left: (t.o.fixedFullWidth && t.isFixed) ? o + 'px' : (o - t.$btnPane.offset().left) + 'px' 1020 }).show(); 1021 1022 $(window).trigger('scroll'); 1023 1024 $('body', d).on('mousedown.' + t.eventNamespace, function (e) { 1025 if (!$dropdown.is(e.target)) { 1026 $('.' + prefix + 'dropdown', d).hide(); 1027 $('.' + prefix + 'active', d).removeClass(prefix + 'active'); 1028 $('body', d).off('mousedown.' + t.eventNamespace); 1029 } 1030 }); 1031 } 1032 }, 1033 1034 1035 // HTML Code management 1036 html: function (html) { 1037 var t = this; 1038 if (html != null) { 1039 t.$ta.val(html); 1040 t.syncCode(true); 1041 return t; 1042 } 1043 return t.$ta.val(); 1044 }, 1045 syncTextarea: function () { 1046 var t = this; 1047 t.$ta.val(t.$ed.text().trim().length > 0 || t.$ed.find('hr,img,embed,iframe,input').length > 0 ? t.$ed.html() : ''); 1048 }, 1049 syncCode: function (force) { 1050 var t = this; 1051 if (!force && t.$ed.is(':visible')) { 1052 t.syncTextarea(); 1053 } else { 1054 // wrap the content in a div it's easier to get the innerhtml 1055 var html = $('<div>').html(t.$ta.val()); 1056 //scrub the html before loading into the doc 1057 var safe = $('<div>').append(html); 1058 $(t.o.tagsToRemove.join(','), safe).remove(); 1059 t.$ed.html(safe.contents().html()); 1060 } 1061 1062 if (t.o.autogrow) { 1063 t.height = t.$ed.height(); 1064 if (t.height !== t.$ta.css('height')) { 1065 t.$ta.css({height: t.height}); 1066 t.$c.trigger('tbwresize'); 1067 } 1068 } 1069 if (t.o.autogrowOnEnter) { 1070 // t.autogrowEditorOnEnter(); 1071 t.$ed.height('auto'); 1072 var totalheight = t.autogrowOnEnterWasFocused ? t.$ed[0].scrollHeight : t.$ed.css('min-height'); 1073 if (totalheight !== t.$ta.css('height')) { 1074 t.$ed.css({height: totalheight}); 1075 t.$c.trigger('tbwresize'); 1076 } 1077 } 1078 }, 1079 1080 // Analyse and update to semantic code 1081 // @param force : force to sync code from textarea 1082 // @param full : wrap text nodes in <p> 1083 // @param keepRange : leave selection range as it is 1084 semanticCode: function (force, full, keepRange) { 1085 var t = this; 1086 t.saveRange(); 1087 t.syncCode(force); 1088 1089 if (t.o.semantic) { 1090 t.semanticTag('b', 'strong'); 1091 t.semanticTag('i', 'em'); 1092 t.semanticTag('strike', 'del'); 1093 1094 if (full) { 1095 var inlineElementsSelector = t.o.inlineElementsSelector, 1096 blockElementsSelector = ':not(' + inlineElementsSelector + ')'; 1097 1098 // Wrap text nodes in span for easier processing 1099 t.$ed.contents().filter(function () { 1100 return this.nodeType === 3 && this.nodeValue.trim().length > 0; 1101 }).wrap('<span data-tbw/>'); 1102 1103 // Wrap groups of inline elements in paragraphs (recursive) 1104 var wrapInlinesInParagraphsFrom = function ($from) { 1105 if ($from.length !== 0) { 1106 var $finalParagraph = $from.nextUntil(blockElementsSelector).addBack().wrapAll('<p/>').parent(), 1107 $nextElement = $finalParagraph.nextAll(inlineElementsSelector).first(); 1108 $finalParagraph.next('br').remove(); 1109 wrapInlinesInParagraphsFrom($nextElement); 1110 } 1111 }; 1112 wrapInlinesInParagraphsFrom(t.$ed.children(inlineElementsSelector).first()); 1113 1114 t.semanticTag('div', 'p', true); 1115 1116 // Unwrap paragraphs content, containing nothing usefull 1117 t.$ed.find('p').filter(function () { 1118 // Don't remove currently being edited element 1119 if (t.range && this === t.range.startContainer) { 1120 return false; 1121 } 1122 return $(this).text().trim().length === 0 && $(this).children().not('br,span').length === 0; 1123 }).contents().unwrap(); 1124 1125 // Get rid of temporial span's 1126 $('[data-tbw]', t.$ed).contents().unwrap(); 1127 1128 // Remove empty <p> 1129 t.$ed.find('p:empty').remove(); 1130 } 1131 1132 if (!keepRange) { 1133 t.restoreRange(); 1134 } 1135 1136 t.syncTextarea(); 1137 } 1138 }, 1139 1140 semanticTag: function (oldTag, newTag, copyAttributes) { 1141 $(oldTag, this.$ed).each(function () { 1142 var $oldTag = $(this); 1143 $oldTag.wrap('<' + newTag + '/>'); 1144 if (copyAttributes) { 1145 $.each($oldTag.prop('attributes'), function () { 1146 $oldTag.parent().attr(this.name, this.value); 1147 }); 1148 } 1149 $oldTag.contents().unwrap(); 1150 }); 1151 }, 1152 1153 // Function call when user click on "Insert Link" 1154 createLink: function () { 1155 var t = this, 1156 documentSelection = t.doc.getSelection(), 1157 node = documentSelection.focusNode, 1158 url, 1159 title, 1160 target; 1161 1162 while (['A', 'DIV'].indexOf(node.nodeName) < 0) { 1163 node = node.parentNode; 1164 } 1165 1166 if (node && node.nodeName === 'A') { 1167 var $a = $(node); 1168 url = $a.attr('href'); 1169 title = $a.attr('title'); 1170 target = $a.attr('target'); 1171 var range = t.doc.createRange(); 1172 range.selectNode(node); 1173 documentSelection.removeAllRanges(); 1174 documentSelection.addRange(range); 1175 } 1176 1177 t.saveRange(); 1178 1179 t.openModalInsert(t.lang.createLink, { 1180 url: { 1181 label: 'URL', 1182 required: true, 1183 value: url 1184 }, 1185 title: { 1186 label: t.lang.title, 1187 value: title 1188 }, 1189 text: { 1190 label: t.lang.text, 1191 value: t.getRangeText() 1192 }, 1193 target: { 1194 label: t.lang.target, 1195 value: target 1196 } 1197 }, function (v) { // v is value 1198 var link = $(['<a href="', v.url, '">', v.text, '</a>'].join('')); 1199 if (v.title.length > 0) { 1200 link.attr('title', v.title); 1201 } 1202 if (v.target.length > 0) { 1203 link.attr('target', v.target); 1204 } 1205 t.range.deleteContents(); 1206 t.range.insertNode(link[0]); 1207 return true; 1208 }); 1209 }, 1210 unlink: function () { 1211 var t = this, 1212 documentSelection = t.doc.getSelection(), 1213 node = documentSelection.focusNode; 1214 1215 if (documentSelection.isCollapsed) { 1216 while (['A', 'DIV'].indexOf(node.nodeName) < 0) { 1217 node = node.parentNode; 1218 } 1219 1220 if (node && node.nodeName === 'A') { 1221 var range = t.doc.createRange(); 1222 range.selectNode(node); 1223 documentSelection.removeAllRanges(); 1224 documentSelection.addRange(range); 1225 } 1226 } 1227 t.execCmd('unlink', undefined, undefined, true); 1228 }, 1229 insertImage: function () { 1230 var t = this; 1231 t.saveRange(); 1232 t.openModalInsert(t.lang.insertImage, { 1233 url: { 1234 label: 'URL', 1235 required: true 1236 }, 1237 alt: { 1238 label: t.lang.description, 1239 value: t.getRangeText() 1240 } 1241 }, function (v) { // v are values 1242 t.execCmd('insertImage', v.url); 1243 $('img[src="' + v.url + '"]:not([alt])', t.$box).attr('alt', v.alt); 1244 return true; 1245 }); 1246 }, 1247 fullscreen: function () { 1248 var t = this, 1249 prefix = t.o.prefix, 1250 fullscreenCssClass = prefix + 'fullscreen', 1251 isFullscreen; 1252 1253 t.$box.toggleClass(fullscreenCssClass); 1254 isFullscreen = t.$box.hasClass(fullscreenCssClass); 1255 $('body').toggleClass(prefix + 'body-fullscreen', isFullscreen); 1256 $(window).trigger('scroll'); 1257 t.$c.trigger('tbw' + (isFullscreen ? 'open' : 'close') + 'fullscreen'); 1258 }, 1259 1260 1261 /* 1262 * Call method of trumbowyg if exist 1263 * else try to call anonymous function 1264 * and finaly native execCommand 1265 */ 1266 execCmd: function (cmd, param, forceCss, skipTrumbowyg) { 1267 var t = this; 1268 skipTrumbowyg = !!skipTrumbowyg || ''; 1269 1270 if (cmd !== 'dropdown') { 1271 t.$ed.focus(); 1272 } 1273 1274 try { 1275 t.doc.execCommand('styleWithCSS', false, forceCss || false); 1276 } catch (c) { 1277 } 1278 1279 try { 1280 t[cmd + skipTrumbowyg](param); 1281 } catch (c) { 1282 try { 1283 cmd(param); 1284 } catch (e2) { 1285 if (cmd === 'insertHorizontalRule') { 1286 param = undefined; 1287 } else if (cmd === 'formatBlock' && t.isIE) { 1288 param = '<' + param + '>'; 1289 } 1290 1291 t.doc.execCommand(cmd, false, param); 1292 1293 t.syncCode(); 1294 t.semanticCode(false, true); 1295 } 1296 1297 if (cmd !== 'dropdown') { 1298 t.updateButtonPaneStatus(); 1299 t.$c.trigger('tbwchange'); 1300 } 1301 } 1302 }, 1303 1304 1305 // Open a modal box 1306 openModal: function (title, content) { 1307 var t = this, 1308 prefix = t.o.prefix; 1309 1310 // No open a modal box when exist other modal box 1311 if ($('.' + prefix + 'modal-box', t.$box).length > 0) { 1312 return false; 1313 } 1314 if (t.o.autogrowOnEnter) { 1315 t.autogrowOnEnterDontClose = true; 1316 } 1317 1318 t.saveRange(); 1319 t.showOverlay(); 1320 1321 // Disable all btnPane btns 1322 t.$btnPane.addClass(prefix + 'disable'); 1323 1324 // Build out of ModalBox, it's the mask for animations 1325 var $modal = $('<div/>', { 1326 class: prefix + 'modal ' + prefix + 'fixed-top' 1327 }).css({ 1328 top: t.$btnPane.height() 1329 }).appendTo(t.$box); 1330 1331 // Click on overlay close modal by cancelling them 1332 t.$overlay.one('click', function () { 1333 $modal.trigger(CANCEL_EVENT); 1334 return false; 1335 }); 1336 1337 // Build the form 1338 var $form = $('<form/>', { 1339 action: '', 1340 html: content 1341 }) 1342 .on('submit', function () { 1343 $modal.trigger(CONFIRM_EVENT); 1344 return false; 1345 }) 1346 .on('reset', function () { 1347 $modal.trigger(CANCEL_EVENT); 1348 return false; 1349 }) 1350 .on('submit reset', function () { 1351 if (t.o.autogrowOnEnter) { 1352 t.autogrowOnEnterDontClose = false; 1353 } 1354 }); 1355 1356 1357 // Build ModalBox and animate to show them 1358 var $box = $('<div/>', { 1359 class: prefix + 'modal-box', 1360 html: $form 1361 }) 1362 .css({ 1363 top: '-' + t.$btnPane.outerHeight() + 'px', 1364 opacity: 0 1365 }) 1366 .appendTo($modal) 1367 .animate({ 1368 top: 0, 1369 opacity: 1 1370 }, 100); 1371 1372 1373 // Append title 1374 $('<span/>', { 1375 text: title, 1376 class: prefix + 'modal-title' 1377 }).prependTo($box); 1378 1379 $modal.height($box.outerHeight() + 10); 1380 1381 1382 // Focus in modal box 1383 $('input:first', $box).focus(); 1384 1385 1386 // Append Confirm and Cancel buttons 1387 t.buildModalBtn('submit', $box); 1388 t.buildModalBtn('reset', $box); 1389 1390 1391 $(window).trigger('scroll'); 1392 1393 return $modal; 1394 }, 1395 // @param n is name of modal 1396 buildModalBtn: function (n, $modal) { 1397 var t = this, 1398 prefix = t.o.prefix; 1399 1400 return $('<button/>', { 1401 class: prefix + 'modal-button ' + prefix + 'modal-' + n, 1402 type: n, 1403 text: t.lang[n] || n 1404 }).appendTo($('form', $modal)); 1405 }, 1406 // close current modal box 1407 closeModal: function () { 1408 var t = this, 1409 prefix = t.o.prefix; 1410 1411 t.$btnPane.removeClass(prefix + 'disable'); 1412 t.$overlay.off(); 1413 1414 // Find the modal box 1415 var $modalBox = $('.' + prefix + 'modal-box', t.$box); 1416 1417 $modalBox.animate({ 1418 top: '-' + $modalBox.height() 1419 }, 100, function () { 1420 $modalBox.parent().remove(); 1421 t.hideOverlay(); 1422 }); 1423 1424 t.restoreRange(); 1425 }, 1426 // Preformated build and management modal 1427 openModalInsert: function (title, fields, cmd) { 1428 var t = this, 1429 prefix = t.o.prefix, 1430 lg = t.lang, 1431 html = ''; 1432 1433 $.each(fields, function (fieldName, field) { 1434 var l = field.label, 1435 n = field.name || fieldName, 1436 a = field.attributes || {}; 1437 1438 var attr = Object.keys(a).map(function (prop) { 1439 return prop + '="' + a[prop] + '"'; 1440 }).join(' '); 1441 1442 html += '<label><input type="' + (field.type || 'text') + '" name="' + n + '" value="' + (field.value || '').replace(/"/g, '"') + '"' + attr + '><span class="' + prefix + 'input-infos"><span>' + 1443 ((!l) ? (lg[fieldName] ? lg[fieldName] : fieldName) : (lg[l] ? lg[l] : l)) + 1444 '</span></span></label>'; 1445 }); 1446 1447 return t.openModal(title, html) 1448 .on(CONFIRM_EVENT, function () { 1449 var $form = $('form', $(this)), 1450 valid = true, 1451 values = {}; 1452 1453 $.each(fields, function (fieldName, field) { 1454 var $field = $('input[name="' + fieldName + '"]', $form), 1455 inputType = $field.attr('type'); 1456 1457 if (inputType.toLowerCase() === 'checkbox') { 1458 values[fieldName] = $field.is(':checked'); 1459 } else { 1460 values[fieldName] = $.trim($field.val()); 1461 } 1462 // Validate value 1463 if (field.required && values[fieldName] === '') { 1464 valid = false; 1465 t.addErrorOnModalField($field, t.lang.required); 1466 } else if (field.pattern && !field.pattern.test(values[fieldName])) { 1467 valid = false; 1468 t.addErrorOnModalField($field, field.patternError); 1469 } 1470 }); 1471 1472 if (valid) { 1473 t.restoreRange(); 1474 1475 if (cmd(values, fields)) { 1476 t.syncCode(); 1477 t.$c.trigger('tbwchange'); 1478 t.closeModal(); 1479 $(this).off(CONFIRM_EVENT); 1480 } 1481 } 1482 }) 1483 .one(CANCEL_EVENT, function () { 1484 $(this).off(CONFIRM_EVENT); 1485 t.closeModal(); 1486 }); 1487 }, 1488 addErrorOnModalField: function ($field, err) { 1489 var prefix = this.o.prefix, 1490 $label = $field.parent(); 1491 1492 $field 1493 .on('change keyup', function () { 1494 $label.removeClass(prefix + 'input-error'); 1495 }); 1496 1497 $label 1498 .addClass(prefix + 'input-error') 1499 .find('input+span') 1500 .append( 1501 $('<span/>', { 1502 class: prefix + 'msg-error', 1503 text: err 1504 }) 1505 ); 1506 }, 1507 1508 getDefaultImgDblClickHandler: function () { 1509 var t = this; 1510 1511 return function () { 1512 var $img = $(this), 1513 src = $img.attr('src'), 1514 base64 = '(Base64)'; 1515 1516 if (src.indexOf('data:image') === 0) { 1517 src = base64; 1518 } 1519 1520 t.openModalInsert(t.lang.insertImage, { 1521 url: { 1522 label: 'URL', 1523 value: src, 1524 required: true 1525 }, 1526 alt: { 1527 label: t.lang.description, 1528 value: $img.attr('alt') 1529 } 1530 }, function (v) { 1531 if (v.src !== base64) { 1532 $img.attr({ 1533 src: v.src 1534 }); 1535 } 1536 $img.attr({ 1537 alt: v.alt 1538 }); 1539 return true; 1540 }); 1541 return false; 1542 }; 1543 }, 1544 1545 // Range management 1546 saveRange: function () { 1547 var t = this, 1548 documentSelection = t.doc.getSelection(); 1549 1550 t.range = null; 1551 1552 if (documentSelection.rangeCount) { 1553 var savedRange = t.range = documentSelection.getRangeAt(0), 1554 range = t.doc.createRange(), 1555 rangeStart; 1556 range.selectNodeContents(t.$ed[0]); 1557 range.setEnd(savedRange.startContainer, savedRange.startOffset); 1558 rangeStart = (range + '').length; 1559 t.metaRange = { 1560 start: rangeStart, 1561 end: rangeStart + (savedRange + '').length 1562 }; 1563 } 1564 }, 1565 restoreRange: function () { 1566 var t = this, 1567 metaRange = t.metaRange, 1568 savedRange = t.range, 1569 documentSelection = t.doc.getSelection(), 1570 range; 1571 1572 if (!savedRange) { 1573 return; 1574 } 1575 1576 if (metaRange && metaRange.start !== metaRange.end) { // Algorithm from http://jsfiddle.net/WeWy7/3/ 1577 var charIndex = 0, 1578 nodeStack = [t.$ed[0]], 1579 node, 1580 foundStart = false, 1581 stop = false; 1582 1583 range = t.doc.createRange(); 1584 1585 while (!stop && (node = nodeStack.pop())) { 1586 if (node.nodeType === 3) { 1587 var nextCharIndex = charIndex + node.length; 1588 if (!foundStart && metaRange.start >= charIndex && metaRange.start <= nextCharIndex) { 1589 range.setStart(node, metaRange.start - charIndex); 1590 foundStart = true; 1591 } 1592 if (foundStart && metaRange.end >= charIndex && metaRange.end <= nextCharIndex) { 1593 range.setEnd(node, metaRange.end - charIndex); 1594 stop = true; 1595 } 1596 charIndex = nextCharIndex; 1597 } else { 1598 var cn = node.childNodes, 1599 i = cn.length; 1600 1601 while (i > 0) { 1602 i -= 1; 1603 nodeStack.push(cn[i]); 1604 } 1605 } 1606 } 1607 } 1608 1609 documentSelection.removeAllRanges(); 1610 documentSelection.addRange(range || savedRange); 1611 }, 1612 getRangeText: function () { 1613 return this.range + ''; 1614 }, 1615 1616 updateButtonPaneStatus: function () { 1617 var t = this, 1618 prefix = t.o.prefix, 1619 tags = t.getTagsRecursive(t.doc.getSelection().focusNode), 1620 activeClasses = prefix + 'active-button ' + prefix + 'active'; 1621 1622 $('.' + prefix + 'active-button', t.$btnPane).removeClass(activeClasses); 1623 $.each(tags, function (i, tag) { 1624 var btnName = t.tagToButton[tag.toLowerCase()], 1625 $btn = $('.' + prefix + btnName + '-button', t.$btnPane); 1626 1627 if ($btn.length > 0) { 1628 $btn.addClass(activeClasses); 1629 } else { 1630 try { 1631 $btn = $('.' + prefix + 'dropdown .' + prefix + btnName + '-dropdown-button', t.$box); 1632 var dropdownBtnName = $btn.parent().data('dropdown'); 1633 $('.' + prefix + dropdownBtnName + '-button', t.$box).addClass(activeClasses); 1634 } catch (e) { 1635 } 1636 } 1637 }); 1638 }, 1639 getTagsRecursive: function (element, tags) { 1640 var t = this; 1641 tags = tags || (element && element.tagName ? [element.tagName] : []); 1642 1643 if (element && element.parentNode) { 1644 element = element.parentNode; 1645 } else { 1646 return tags; 1647 } 1648 1649 var tag = element.tagName; 1650 if (tag === 'DIV') { 1651 return tags; 1652 } 1653 if (tag === 'P' && element.style.textAlign !== '') { 1654 tags.push(element.style.textAlign); 1655 } 1656 1657 $.each(t.tagHandlers, function (i, tagHandler) { 1658 tags = tags.concat(tagHandler(element, t)); 1659 }); 1660 1661 tags.push(tag); 1662 1663 return t.getTagsRecursive(element, tags).filter(function(tag) { return tag != null; }); 1664 }, 1665 1666 // Plugins 1667 initPlugins: function () { 1668 var t = this; 1669 t.loadedPlugins = []; 1670 $.each($.trumbowyg.plugins, function (name, plugin) { 1671 if (!plugin.shouldInit || plugin.shouldInit(t)) { 1672 plugin.init(t); 1673 if (plugin.tagHandler) { 1674 t.tagHandlers.push(plugin.tagHandler); 1675 } 1676 t.loadedPlugins.push(plugin); 1677 } 1678 }); 1679 }, 1680 destroyPlugins: function () { 1681 $.each(this.loadedPlugins, function (i, plugin) { 1682 if (plugin.destroy) { 1683 plugin.destroy(); 1684 } 1685 }); 1686 } 1687 }; 1688 })(navigator, window, document, jQuery);