File modules/editor/trumbowyg/trumbowyg.js

Last commit: Tue Aug 28 00:33:27 2018 +0200	Jan Dankert	Editoren für Markdown (SimpleMDE) und HTML (Trumbowyg) installiert.
1 /** 2 * Trumbowyg v2.10.0 - 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 width: 'Width' 67 } 68 }, 69 70 // Plugins 71 plugins: {}, 72 73 // SVG Path globally 74 svgPath: null, 75 76 hideButtonTexts: null 77 }; 78 79 // Makes default options read-only 80 Object.defineProperty(jQuery.trumbowyg, 'defaultOptions', { 81 value: { 82 lang: 'en', 83 84 fixedBtnPane: false, 85 fixedFullWidth: false, 86 autogrow: false, 87 autogrowOnEnter: false, 88 imageWidthModalEdit: false, 89 90 prefix: 'trumbowyg-', 91 92 semantic: true, 93 resetCss: false, 94 removeformatPasted: false, 95 tagsToRemove: [], 96 btns: [ 97 ['viewHTML'], 98 ['undo', 'redo'], // Only supported in Blink browsers 99 ['formatting'], 100 ['strong', 'em', 'del'], 101 ['superscript', 'subscript'], 102 ['link'], 103 ['insertImage'], 104 ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'], 105 ['unorderedList', 'orderedList'], 106 ['horizontalRule'], 107 ['removeformat'], 108 ['fullscreen'] 109 ], 110 // For custom button definitions 111 btnsDef: {}, 112 113 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', 114 115 pasteHandlers: [], 116 117 // imgDblClickHandler: default is defined in constructor 118 119 plugins: {}, 120 urlProtocol: false, 121 minimalLinks: false 122 }, 123 writable: false, 124 enumerable: true, 125 configurable: false 126 }); 127 128 129 (function (navigator, window, document, $) { 130 'use strict'; 131 132 var CONFIRM_EVENT = 'tbwconfirm', 133 CANCEL_EVENT = 'tbwcancel'; 134 135 $.fn.trumbowyg = function (options, params) { 136 var trumbowygDataName = 'trumbowyg'; 137 if (options === Object(options) || !options) { 138 return this.each(function () { 139 if (!$(this).data(trumbowygDataName)) { 140 $(this).data(trumbowygDataName, new Trumbowyg(this, options)); 141 } 142 }); 143 } 144 if (this.length === 1) { 145 try { 146 var t = $(this).data(trumbowygDataName); 147 switch (options) { 148 // Exec command 149 case 'execCmd': 150 return t.execCmd(params.cmd, params.param, params.forceCss); 151 152 // Modal box 153 case 'openModal': 154 return t.openModal(params.title, params.content); 155 case 'closeModal': 156 return t.closeModal(); 157 case 'openModalInsert': 158 return t.openModalInsert(params.title, params.fields, params.callback); 159 160 // Range 161 case 'saveRange': 162 return t.saveRange(); 163 case 'getRange': 164 return t.range; 165 case 'getRangeText': 166 return t.getRangeText(); 167 case 'restoreRange': 168 return t.restoreRange(); 169 170 // Enable/disable 171 case 'enable': 172 return t.setDisabled(false); 173 case 'disable': 174 return t.setDisabled(true); 175 176 // Toggle 177 case 'toggle': 178 return t.toggle(); 179 180 // Destroy 181 case 'destroy': 182 return t.destroy(); 183 184 // Empty 185 case 'empty': 186 return t.empty(); 187 188 // HTML 189 case 'html': 190 return t.html(params); 191 } 192 } catch (c) { 193 } 194 } 195 196 return false; 197 }; 198 199 // @param: editorElem is the DOM element 200 var Trumbowyg = function (editorElem, options) { 201 var t = this, 202 trumbowygIconsId = 'trumbowyg-icons', 203 $trumbowyg = $.trumbowyg; 204 205 // Get the document of the element. It use to makes the plugin 206 // compatible on iframes. 207 t.doc = editorElem.ownerDocument || document; 208 209 // jQuery object of the editor 210 t.$ta = $(editorElem); // $ta : Textarea 211 t.$c = $(editorElem); // $c : creator 212 213 options = options || {}; 214 215 // Localization management 216 if (options.lang != null || $trumbowyg.langs[options.lang] != null) { 217 t.lang = $.extend(true, {}, $trumbowyg.langs.en, $trumbowyg.langs[options.lang]); 218 } else { 219 t.lang = $trumbowyg.langs.en; 220 } 221 222 t.hideButtonTexts = $trumbowyg.hideButtonTexts != null ? $trumbowyg.hideButtonTexts : options.hideButtonTexts; 223 224 // SVG path 225 var svgPathOption = $trumbowyg.svgPath != null ? $trumbowyg.svgPath : options.svgPath; 226 t.hasSvg = svgPathOption !== false; 227 t.svgPath = !!t.doc.querySelector('base') ? window.location.href.split('#')[0] : ''; 228 if ($('#' + trumbowygIconsId, t.doc).length === 0 && svgPathOption !== false) { 229 if (svgPathOption == null) { 230 // Hack to get svgPathOption based on trumbowyg.js path 231 var scriptElements = document.getElementsByTagName('script'); 232 for (var i = 0; i < scriptElements.length; i += 1) { 233 var source = scriptElements[i].src; 234 var matches = source.match('trumbowyg(\.min)?\.js'); 235 if (matches != null) { 236 svgPathOption = source.substring(0, source.indexOf(matches[0])) + 'ui/icons.svg'; 237 } 238 } 239 if (svgPathOption == null) { 240 console.warn('You must define svgPath: https://goo.gl/CfTY9U'); // jshint ignore:line 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.urlPrefix = t.setupUrlPrefix(); 415 416 t.disabled = t.o.disabled || (editorElem.nodeName === 'TEXTAREA' && editorElem.disabled); 417 418 if (options.btns) { 419 t.o.btns = options.btns; 420 } else if (!t.o.semantic) { 421 t.o.btns[3] = ['bold', 'italic', 'underline', 'strikethrough']; 422 } 423 424 $.each(t.o.btnsDef, function (btnName, btnDef) { 425 t.addBtnDef(btnName, btnDef); 426 }); 427 428 // put this here in the event it would be merged in with options 429 t.eventNamespace = 'trumbowyg-event'; 430 431 // Keyboard shortcuts are load in this array 432 t.keys = []; 433 434 // Tag to button dynamically hydrated 435 t.tagToButton = {}; 436 t.tagHandlers = []; 437 438 // Admit multiple paste handlers 439 t.pasteHandlers = [].concat(t.o.pasteHandlers); 440 441 // Check if browser is IE 442 t.isIE = (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') !== -1); 443 444 t.init(); 445 }; 446 447 Trumbowyg.prototype = { 448 DEFAULT_SEMANTIC_MAP: { 449 'b': 'strong', 450 'i': 'em', 451 's': 'del', 452 'strike': 'del', 453 'div': 'p' 454 }, 455 456 init: function () { 457 var t = this; 458 t.height = t.$ta.height(); 459 460 t.initPlugins(); 461 462 try { 463 // Disable image resize, try-catch for old IE 464 t.doc.execCommand('enableObjectResizing', false, false); 465 t.doc.execCommand('defaultParagraphSeparator', false, 'p'); 466 } catch (e) { 467 } 468 469 t.buildEditor(); 470 t.buildBtnPane(); 471 472 t.fixedBtnPaneEvents(); 473 474 t.buildOverlay(); 475 476 setTimeout(function () { 477 if (t.disabled) { 478 t.setDisabled(true); 479 } 480 t.$c.trigger('tbwinit'); 481 }); 482 }, 483 484 addBtnDef: function (btnName, btnDef) { 485 this.btnsDef[btnName] = btnDef; 486 }, 487 488 setupUrlPrefix: function () { 489 var protocol = this.o.urlProtocol; 490 if (!protocol) { 491 return; 492 } 493 494 if (typeof(protocol) !== 'string') { 495 return 'https://'; 496 } 497 return /:\/\/$/.test(protocol) ? protocol : protocol + '://'; 498 }, 499 500 buildEditor: function () { 501 var t = this, 502 prefix = t.o.prefix, 503 html = ''; 504 505 t.$box = $('<div/>', { 506 class: prefix + 'box ' + prefix + 'editor-visible ' + prefix + t.o.lang + ' trumbowyg' 507 }); 508 509 // $ta = Textarea 510 // $ed = Editor 511 t.isTextarea = t.$ta.is('textarea'); 512 if (t.isTextarea) { 513 html = t.$ta.val(); 514 t.$ed = $('<div/>'); 515 t.$box 516 .insertAfter(t.$ta) 517 .append(t.$ed, t.$ta); 518 } else { 519 t.$ed = t.$ta; 520 html = t.$ed.html(); 521 522 t.$ta = $('<textarea/>', { 523 name: t.$ta.attr('id'), 524 height: t.height 525 }).val(html); 526 527 t.$box 528 .insertAfter(t.$ed) 529 .append(t.$ta, t.$ed); 530 t.syncCode(); 531 } 532 533 t.$ta 534 .addClass(prefix + 'textarea') 535 .attr('tabindex', -1) 536 ; 537 538 t.$ed 539 .addClass(prefix + 'editor') 540 .attr({ 541 contenteditable: true, 542 dir: t.lang._dir || 'ltr' 543 }) 544 .html(html) 545 ; 546 547 if (t.o.tabindex) { 548 t.$ed.attr('tabindex', t.o.tabindex); 549 } 550 551 if (t.$c.is('[placeholder]')) { 552 t.$ed.attr('placeholder', t.$c.attr('placeholder')); 553 } 554 555 if (t.$c.is('[spellcheck]')) { 556 t.$ed.attr('spellcheck', t.$c.attr('spellcheck')); 557 } 558 559 if (t.o.resetCss) { 560 t.$ed.addClass(prefix + 'reset-css'); 561 } 562 563 if (!t.o.autogrow) { 564 t.$ta.add(t.$ed).css({ 565 height: t.height 566 }); 567 } 568 569 t.semanticCode(); 570 571 if (t.o.autogrowOnEnter) { 572 t.$ed.addClass(prefix + 'autogrow-on-enter'); 573 } 574 575 var ctrl = false, 576 composition = false, 577 debounceButtonPaneStatus, 578 updateEventName = 'keyup'; 579 580 t.$ed 581 .on('dblclick', 'img', t.o.imgDblClickHandler) 582 .on('keydown', function (e) { 583 if ((e.ctrlKey || e.metaKey) && !e.altKey) { 584 ctrl = true; 585 var key = t.keys[String.fromCharCode(e.which).toUpperCase()]; 586 587 try { 588 t.execCmd(key.fn, key.param); 589 return false; 590 } catch (c) { 591 } 592 } 593 }) 594 .on('compositionstart compositionupdate', function () { 595 composition = true; 596 }) 597 .on(updateEventName + ' compositionend', function (e) { 598 if (e.type === 'compositionend') { 599 composition = false; 600 } else if (composition) { 601 return; 602 } 603 604 var keyCode = e.which; 605 606 if (keyCode >= 37 && keyCode <= 40) { 607 return; 608 } 609 610 if ((e.ctrlKey || e.metaKey) && (keyCode === 89 || keyCode === 90)) { 611 t.$c.trigger('tbwchange'); 612 } else if (!ctrl && keyCode !== 17) { 613 var compositionEndIE = t.isIE ? e.type === 'compositionend' : true; 614 t.semanticCode(false, compositionEndIE && keyCode === 13); 615 t.$c.trigger('tbwchange'); 616 } else if (typeof e.which === 'undefined') { 617 t.semanticCode(false, false, true); 618 } 619 620 setTimeout(function () { 621 ctrl = false; 622 }, 50); 623 }) 624 .on('mouseup keydown keyup', function (e) { 625 if ((!e.ctrlKey && !e.metaKey) || e.altKey) { 626 setTimeout(function () { // "hold on" to the ctrl key for 50ms 627 ctrl = false; 628 }, 50); 629 } 630 clearTimeout(debounceButtonPaneStatus); 631 debounceButtonPaneStatus = setTimeout(function () { 632 t.updateButtonPaneStatus(); 633 }, 50); 634 }) 635 .on('focus blur', function (e) { 636 t.$c.trigger('tbw' + e.type); 637 if (e.type === 'blur') { 638 $('.' + prefix + 'active-button', t.$btnPane).removeClass(prefix + 'active-button ' + prefix + 'active'); 639 } 640 if (t.o.autogrowOnEnter) { 641 if (t.autogrowOnEnterDontClose) { 642 return; 643 } 644 if (e.type === 'focus') { 645 t.autogrowOnEnterWasFocused = true; 646 t.autogrowEditorOnEnter(); 647 } 648 else if (!t.o.autogrow) { 649 t.$ed.css({height: t.$ed.css('min-height')}); 650 t.$c.trigger('tbwresize'); 651 } 652 } 653 }) 654 .on('cut', function () { 655 setTimeout(function () { 656 t.semanticCode(false, true); 657 t.$c.trigger('tbwchange'); 658 }, 0); 659 }) 660 .on('paste', function (e) { 661 if (t.o.removeformatPasted) { 662 e.preventDefault(); 663 664 if (window.getSelection && window.getSelection().deleteFromDocument) { 665 window.getSelection().deleteFromDocument(); 666 } 667 668 try { 669 // IE 670 var text = window.clipboardData.getData('Text'); 671 672 try { 673 // <= IE10 674 t.doc.selection.createRange().pasteHTML(text); 675 } catch (c) { 676 // IE 11 677 t.doc.getSelection().getRangeAt(0).insertNode(t.doc.createTextNode(text)); 678 } 679 t.$c.trigger('tbwchange', e); 680 } catch (d) { 681 // Not IE 682 t.execCmd('insertText', (e.originalEvent || e).clipboardData.getData('text/plain')); 683 } 684 } 685 686 // Call pasteHandlers 687 $.each(t.pasteHandlers, function (i, pasteHandler) { 688 pasteHandler(e); 689 }); 690 691 setTimeout(function () { 692 t.semanticCode(false, true); 693 t.$c.trigger('tbwpaste', e); 694 }, 0); 695 }); 696 697 t.$ta 698 .on('keyup', function () { 699 t.$c.trigger('tbwchange'); 700 }) 701 .on('paste', function () { 702 setTimeout(function () { 703 t.$c.trigger('tbwchange'); 704 }, 0); 705 }); 706 707 t.$box.on('keydown', function (e) { 708 if (e.which === 27 && $('.' + prefix + 'modal-box', t.$box).length === 1) { 709 t.closeModal(); 710 return false; 711 } 712 }); 713 }, 714 715 //autogrow when entering logic 716 autogrowEditorOnEnter: function () { 717 var t = this; 718 t.$ed.removeClass('autogrow-on-enter'); 719 var oldHeight = t.$ed[0].clientHeight; 720 t.$ed.height('auto'); 721 var totalHeight = t.$ed[0].scrollHeight; 722 t.$ed.addClass('autogrow-on-enter'); 723 if (oldHeight !== totalHeight) { 724 t.$ed.height(oldHeight); 725 setTimeout(function () { 726 t.$ed.css({height: totalHeight}); 727 t.$c.trigger('tbwresize'); 728 }, 0); 729 } 730 }, 731 732 733 // Build button pane, use o.btns option 734 buildBtnPane: function () { 735 var t = this, 736 prefix = t.o.prefix; 737 738 var $btnPane = t.$btnPane = $('<div/>', { 739 class: prefix + 'button-pane' 740 }); 741 742 $.each(t.o.btns, function (i, btnGrp) { 743 if (!$.isArray(btnGrp)) { 744 btnGrp = [btnGrp]; 745 } 746 747 var $btnGroup = $('<div/>', { 748 class: prefix + 'button-group ' + ((btnGrp.indexOf('fullscreen') >= 0) ? prefix + 'right' : '') 749 }); 750 $.each(btnGrp, function (i, btn) { 751 try { // Prevent buildBtn error 752 if (t.isSupportedBtn(btn)) { // It's a supported button 753 $btnGroup.append(t.buildBtn(btn)); 754 } 755 } catch (c) { 756 } 757 }); 758 759 if ($btnGroup.html().trim().length > 0) { 760 $btnPane.append($btnGroup); 761 } 762 }); 763 764 t.$box.prepend($btnPane); 765 }, 766 767 768 // Build a button and his action 769 buildBtn: function (btnName) { // btnName is name of the button 770 var t = this, 771 prefix = t.o.prefix, 772 btn = t.btnsDef[btnName], 773 isDropdown = btn.dropdown, 774 hasIcon = btn.hasIcon != null ? btn.hasIcon : true, 775 textDef = t.lang[btnName] || btnName, 776 777 $btn = $('<button/>', { 778 type: 'button', 779 class: prefix + btnName + '-button ' + (btn.class || '') + (!hasIcon ? ' ' + prefix + 'textual-button' : ''), 780 html: t.hasSvg && hasIcon ? 781 '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' : 782 t.hideButtonTexts ? '' : (btn.text || btn.title || t.lang[btnName] || btnName), 783 title: (btn.title || btn.text || textDef) + ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : ''), 784 tabindex: -1, 785 mousedown: function () { 786 if (!isDropdown || $('.' + btnName + '-' + prefix + 'dropdown', t.$box).is(':hidden')) { 787 $('body', t.doc).trigger('mousedown'); 788 } 789 790 if ((t.$btnPane.hasClass(prefix + 'disable') || t.$box.hasClass(prefix + 'disabled')) && 791 !$(this).hasClass(prefix + 'active') && 792 !$(this).hasClass(prefix + 'not-disable')) { 793 return false; 794 } 795 796 t.execCmd((isDropdown ? 'dropdown' : false) || btn.fn || btnName, btn.param || btnName, btn.forceCss); 797 798 return false; 799 } 800 }); 801 802 if (isDropdown) { 803 $btn.addClass(prefix + 'open-dropdown'); 804 var dropdownPrefix = prefix + 'dropdown', 805 dropdownOptions = { // the dropdown 806 class: dropdownPrefix + '-' + btnName + ' ' + dropdownPrefix + ' ' + prefix + 'fixed-top' 807 }; 808 dropdownOptions['data-' + dropdownPrefix] = btnName; 809 var $dropdown = $('<div/>', dropdownOptions); 810 $.each(isDropdown, function (i, def) { 811 if (t.btnsDef[def] && t.isSupportedBtn(def)) { 812 $dropdown.append(t.buildSubBtn(def)); 813 } 814 }); 815 t.$box.append($dropdown.hide()); 816 } else if (btn.key) { 817 t.keys[btn.key] = { 818 fn: btn.fn || btnName, 819 param: btn.param || btnName 820 }; 821 } 822 823 if (!isDropdown) { 824 t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName; 825 } 826 827 return $btn; 828 }, 829 // Build a button for dropdown menu 830 // @param n : name of the subbutton 831 buildSubBtn: function (btnName) { 832 var t = this, 833 prefix = t.o.prefix, 834 btn = t.btnsDef[btnName], 835 hasIcon = btn.hasIcon != null ? btn.hasIcon : true; 836 837 if (btn.key) { 838 t.keys[btn.key] = { 839 fn: btn.fn || btnName, 840 param: btn.param || btnName 841 }; 842 } 843 844 t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName; 845 846 return $('<button/>', { 847 type: 'button', 848 class: prefix + btnName + '-dropdown-button' + (btn.ico ? ' ' + prefix + btn.ico + '-button' : ''), 849 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), 850 title: ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : null), 851 style: btn.style || null, 852 mousedown: function () { 853 $('body', t.doc).trigger('mousedown'); 854 855 t.execCmd(btn.fn || btnName, btn.param || btnName, btn.forceCss); 856 857 return false; 858 } 859 }); 860 }, 861 // Check if button is supported 862 isSupportedBtn: function (b) { 863 try { 864 return this.btnsDef[b].isSupported(); 865 } catch (c) { 866 } 867 return true; 868 }, 869 870 // Build overlay for modal box 871 buildOverlay: function () { 872 var t = this; 873 t.$overlay = $('<div/>', { 874 class: t.o.prefix + 'overlay' 875 }).appendTo(t.$box); 876 return t.$overlay; 877 }, 878 showOverlay: function () { 879 var t = this; 880 $(window).trigger('scroll'); 881 t.$overlay.fadeIn(200); 882 t.$box.addClass(t.o.prefix + 'box-blur'); 883 }, 884 hideOverlay: function () { 885 var t = this; 886 t.$overlay.fadeOut(50); 887 t.$box.removeClass(t.o.prefix + 'box-blur'); 888 }, 889 890 // Management of fixed button pane 891 fixedBtnPaneEvents: function () { 892 var t = this, 893 fixedFullWidth = t.o.fixedFullWidth, 894 $box = t.$box; 895 896 if (!t.o.fixedBtnPane) { 897 return; 898 } 899 900 t.isFixed = false; 901 902 $(window) 903 .on('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace, function () { 904 if (!$box) { 905 return; 906 } 907 908 t.syncCode(); 909 910 var scrollTop = $(window).scrollTop(), 911 offset = $box.offset().top + 1, 912 bp = t.$btnPane, 913 oh = bp.outerHeight() - 2; 914 915 if ((scrollTop - offset > 0) && ((scrollTop - offset - t.height) < 0)) { 916 if (!t.isFixed) { 917 t.isFixed = true; 918 bp.css({ 919 position: 'fixed', 920 top: 0, 921 left: fixedFullWidth ? '0' : 'auto', 922 zIndex: 7 923 }); 924 $([t.$ta, t.$ed]).css({marginTop: bp.height()}); 925 } 926 bp.css({ 927 width: fixedFullWidth ? '100%' : (($box.width() - 1) + 'px') 928 }); 929 930 $('.' + t.o.prefix + 'fixed-top', $box).css({ 931 position: fixedFullWidth ? 'fixed' : 'absolute', 932 top: fixedFullWidth ? oh : oh + (scrollTop - offset) + 'px', 933 zIndex: 15 934 }); 935 } else if (t.isFixed) { 936 t.isFixed = false; 937 bp.removeAttr('style'); 938 $([t.$ta, t.$ed]).css({marginTop: 0}); 939 $('.' + t.o.prefix + 'fixed-top', $box).css({ 940 position: 'absolute', 941 top: oh 942 }); 943 } 944 }); 945 }, 946 947 // Disable editor 948 setDisabled: function (disable) { 949 var t = this, 950 prefix = t.o.prefix; 951 952 t.disabled = disable; 953 954 if (disable) { 955 t.$ta.attr('disabled', true); 956 } else { 957 t.$ta.removeAttr('disabled'); 958 } 959 t.$box.toggleClass(prefix + 'disabled', disable); 960 t.$ed.attr('contenteditable', !disable); 961 }, 962 963 // Destroy the editor 964 destroy: function () { 965 var t = this, 966 prefix = t.o.prefix; 967 968 if (t.isTextarea) { 969 t.$box.after( 970 t.$ta 971 .css({height: ''}) 972 .val(t.html()) 973 .removeClass(prefix + 'textarea') 974 .show() 975 ); 976 } else { 977 t.$box.after( 978 t.$ed 979 .css({height: ''}) 980 .removeClass(prefix + 'editor') 981 .removeAttr('contenteditable') 982 .removeAttr('dir') 983 .html(t.html()) 984 .show() 985 ); 986 } 987 988 t.$ed.off('dblclick', 'img'); 989 990 t.destroyPlugins(); 991 992 t.$box.remove(); 993 t.$c.removeData('trumbowyg'); 994 $('body').removeClass(prefix + 'body-fullscreen'); 995 t.$c.trigger('tbwclose'); 996 $(window).off('scroll.' + t.eventNamespace + ' resize.' + t.eventNamespace); 997 }, 998 999 1000 // Empty the editor 1001 empty: function () { 1002 this.$ta.val(''); 1003 this.syncCode(true); 1004 }, 1005 1006 1007 // Function call when click on viewHTML button 1008 toggle: function () { 1009 var t = this, 1010 prefix = t.o.prefix; 1011 1012 if (t.o.autogrowOnEnter) { 1013 t.autogrowOnEnterDontClose = !t.$box.hasClass(prefix + 'editor-hidden'); 1014 } 1015 1016 t.semanticCode(false, true); 1017 1018 setTimeout(function () { 1019 t.doc.activeElement.blur(); 1020 t.$box.toggleClass(prefix + 'editor-hidden ' + prefix + 'editor-visible'); 1021 t.$btnPane.toggleClass(prefix + 'disable'); 1022 $('.' + prefix + 'viewHTML-button', t.$btnPane).toggleClass(prefix + 'active'); 1023 if (t.$box.hasClass(prefix + 'editor-visible')) { 1024 t.$ta.attr('tabindex', -1); 1025 } else { 1026 t.$ta.removeAttr('tabindex'); 1027 } 1028 1029 if (t.o.autogrowOnEnter && !t.autogrowOnEnterDontClose) { 1030 t.autogrowEditorOnEnter(); 1031 } 1032 }, 0); 1033 }, 1034 1035 // Open dropdown when click on a button which open that 1036 dropdown: function (name) { 1037 var t = this, 1038 d = t.doc, 1039 prefix = t.o.prefix, 1040 $dropdown = $('[data-' + prefix + 'dropdown=' + name + ']', t.$box), 1041 $btn = $('.' + prefix + name + '-button', t.$btnPane), 1042 show = $dropdown.is(':hidden'); 1043 1044 $('body', d).trigger('mousedown'); 1045 1046 if (show) { 1047 var o = $btn.offset().left; 1048 $btn.addClass(prefix + 'active'); 1049 1050 $dropdown.css({ 1051 position: 'absolute', 1052 top: $btn.offset().top - t.$btnPane.offset().top + $btn.outerHeight(), 1053 left: (t.o.fixedFullWidth && t.isFixed) ? o + 'px' : (o - t.$btnPane.offset().left) + 'px' 1054 }).show(); 1055 1056 $(window).trigger('scroll'); 1057 1058 $('body', d).on('mousedown.' + t.eventNamespace, function (e) { 1059 if (!$dropdown.is(e.target)) { 1060 $('.' + prefix + 'dropdown', t.$box).hide(); 1061 $('.' + prefix + 'active', t.$btnPane).removeClass(prefix + 'active'); 1062 $('body', d).off('mousedown.' + t.eventNamespace); 1063 } 1064 }); 1065 } 1066 }, 1067 1068 1069 // HTML Code management 1070 html: function (html) { 1071 var t = this; 1072 1073 if (html != null) { 1074 t.$ta.val(html); 1075 t.syncCode(true); 1076 t.$c.trigger('tbwchange'); 1077 return t; 1078 } 1079 1080 return t.$ta.val(); 1081 }, 1082 syncTextarea: function () { 1083 var t = this; 1084 t.$ta.val(t.$ed.text().trim().length > 0 || t.$ed.find('hr,img,embed,iframe,input').length > 0 ? t.$ed.html() : ''); 1085 }, 1086 syncCode: function (force) { 1087 var t = this; 1088 if (!force && t.$ed.is(':visible')) { 1089 t.syncTextarea(); 1090 } else { 1091 // wrap the content in a div it's easier to get the innerhtml 1092 var html = $('<div>').html(t.$ta.val()); 1093 //scrub the html before loading into the doc 1094 var safe = $('<div>').append(html); 1095 $(t.o.tagsToRemove.join(','), safe).remove(); 1096 t.$ed.html(safe.contents().html()); 1097 } 1098 1099 if (t.o.autogrow) { 1100 t.height = t.$ed.height(); 1101 if (t.height !== t.$ta.css('height')) { 1102 t.$ta.css({height: t.height}); 1103 t.$c.trigger('tbwresize'); 1104 } 1105 } 1106 if (t.o.autogrowOnEnter) { 1107 // t.autogrowEditorOnEnter(); 1108 t.$ed.height('auto'); 1109 var totalheight = t.autogrowOnEnterWasFocused ? t.$ed[0].scrollHeight : t.$ed.css('min-height'); 1110 if (totalheight !== t.$ta.css('height')) { 1111 t.$ed.css({height: totalheight}); 1112 t.$c.trigger('tbwresize'); 1113 } 1114 } 1115 }, 1116 1117 // Analyse and update to semantic code 1118 // @param force : force to sync code from textarea 1119 // @param full : wrap text nodes in <p> 1120 // @param keepRange : leave selection range as it is 1121 semanticCode: function (force, full, keepRange) { 1122 var t = this; 1123 t.saveRange(); 1124 t.syncCode(force); 1125 1126 if (t.o.semantic) { 1127 t.semanticTag('b'); 1128 t.semanticTag('i'); 1129 t.semanticTag('s'); 1130 t.semanticTag('strike'); 1131 1132 if (full) { 1133 var inlineElementsSelector = t.o.inlineElementsSelector, 1134 blockElementsSelector = ':not(' + inlineElementsSelector + ')'; 1135 1136 // Wrap text nodes in span for easier processing 1137 t.$ed.contents().filter(function () { 1138 return this.nodeType === 3 && this.nodeValue.trim().length > 0; 1139 }).wrap('<span data-tbw/>'); 1140 1141 // Wrap groups of inline elements in paragraphs (recursive) 1142 var wrapInlinesInParagraphsFrom = function ($from) { 1143 if ($from.length !== 0) { 1144 var $finalParagraph = $from.nextUntil(blockElementsSelector).addBack().wrapAll('<p/>').parent(), 1145 $nextElement = $finalParagraph.nextAll(inlineElementsSelector).first(); 1146 $finalParagraph.next('br').remove(); 1147 wrapInlinesInParagraphsFrom($nextElement); 1148 } 1149 }; 1150 wrapInlinesInParagraphsFrom(t.$ed.children(inlineElementsSelector).first()); 1151 1152 t.semanticTag('div', true); 1153 1154 // Unwrap paragraphs content, containing nothing usefull 1155 t.$ed.find('p').filter(function () { 1156 // Don't remove currently being edited element 1157 if (t.range && this === t.range.startContainer) { 1158 return false; 1159 } 1160 return $(this).text().trim().length === 0 && $(this).children().not('br,span').length === 0; 1161 }).contents().unwrap(); 1162 1163 // Get rid of temporial span's 1164 $('[data-tbw]', t.$ed).contents().unwrap(); 1165 1166 // Remove empty <p> 1167 t.$ed.find('p:empty').remove(); 1168 } 1169 1170 if (!keepRange) { 1171 t.restoreRange(); 1172 } 1173 1174 t.syncTextarea(); 1175 } 1176 }, 1177 1178 semanticTag: function (oldTag, copyAttributes) { 1179 var newTag; 1180 1181 if (this.o.semantic != null && typeof this.o.semantic === 'object' && this.o.semantic.hasOwnProperty(oldTag)) { 1182 newTag = this.o.semantic[oldTag]; 1183 } else if (this.o.semantic === true && this.DEFAULT_SEMANTIC_MAP.hasOwnProperty(oldTag)) { 1184 newTag = this.DEFAULT_SEMANTIC_MAP[oldTag]; 1185 } else { 1186 return; 1187 } 1188 1189 $(oldTag, this.$ed).each(function () { 1190 var $oldTag = $(this); 1191 $oldTag.wrap('<' + newTag + '/>'); 1192 if (copyAttributes) { 1193 $.each($oldTag.prop('attributes'), function () { 1194 $oldTag.parent().attr(this.name, this.value); 1195 }); 1196 } 1197 $oldTag.contents().unwrap(); 1198 }); 1199 }, 1200 1201 // Function call when user click on "Insert Link" 1202 createLink: function () { 1203 var t = this, 1204 documentSelection = t.doc.getSelection(), 1205 node = documentSelection.focusNode, 1206 text = new XMLSerializer().serializeToString(documentSelection.getRangeAt(0).cloneContents()), 1207 url, 1208 title, 1209 target; 1210 1211 while (['A', 'DIV'].indexOf(node.nodeName) < 0) { 1212 node = node.parentNode; 1213 } 1214 1215 if (node && node.nodeName === 'A') { 1216 var $a = $(node); 1217 text = $a.text(); 1218 url = $a.attr('href'); 1219 if (!t.o.minimalLinks) { 1220 title = $a.attr('title'); 1221 target = $a.attr('target'); 1222 } 1223 var range = t.doc.createRange(); 1224 range.selectNode(node); 1225 documentSelection.removeAllRanges(); 1226 documentSelection.addRange(range); 1227 } 1228 1229 t.saveRange(); 1230 1231 var options = { 1232 url: { 1233 label: 'URL', 1234 required: true, 1235 value: url 1236 }, 1237 text: { 1238 label: t.lang.text, 1239 value: text 1240 } 1241 }; 1242 if (!t.o.minimalLinks) { 1243 Object.assign(options, { 1244 title: { 1245 label: t.lang.title, 1246 value: title 1247 }, 1248 target: { 1249 label: t.lang.target, 1250 value: target 1251 } 1252 }); 1253 } 1254 1255 t.openModalInsert(t.lang.createLink, options, function (v) { // v is value 1256 var url = t.prependUrlPrefix(v.url); 1257 if (!url.length) { 1258 return false; 1259 } 1260 1261 var link = $(['<a href="', v.url, '">', v.text || v.url, '</a>'].join('')); 1262 1263 if (!t.o.minimalLinks) { 1264 if (v.title.length > 0) { 1265 link.attr('title', v.title); 1266 } 1267 if (v.target.length > 0) { 1268 link.attr('target', v.target); 1269 } 1270 } 1271 t.range.deleteContents(); 1272 t.range.insertNode(link[0]); 1273 t.syncCode(); 1274 t.$c.trigger('tbwchange'); 1275 return true; 1276 }); 1277 }, 1278 prependUrlPrefix: function (url) { 1279 var t = this; 1280 if (!t.urlPrefix) { 1281 return url; 1282 } 1283 1284 const VALID_LINK_PREFIX = /^([a-z][-+.a-z0-9]*:|\/|#)/i; 1285 if (VALID_LINK_PREFIX.test(url)) { 1286 return url; 1287 } 1288 1289 const SIMPLE_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 1290 if (SIMPLE_EMAIL_REGEX.test(url)) { 1291 return 'mailto:' + url; 1292 } 1293 1294 return t.urlPrefix + url; 1295 }, 1296 unlink: function () { 1297 var t = this, 1298 documentSelection = t.doc.getSelection(), 1299 node = documentSelection.focusNode; 1300 1301 if (documentSelection.isCollapsed) { 1302 while (['A', 'DIV'].indexOf(node.nodeName) < 0) { 1303 node = node.parentNode; 1304 } 1305 1306 if (node && node.nodeName === 'A') { 1307 var range = t.doc.createRange(); 1308 range.selectNode(node); 1309 documentSelection.removeAllRanges(); 1310 documentSelection.addRange(range); 1311 } 1312 } 1313 t.execCmd('unlink', undefined, undefined, true); 1314 }, 1315 insertImage: function () { 1316 var t = this; 1317 t.saveRange(); 1318 1319 var options = { 1320 url: { 1321 label: 'URL', 1322 required: true 1323 }, 1324 alt: { 1325 label: t.lang.description, 1326 value: t.getRangeText() 1327 } 1328 }; 1329 1330 if (t.o.imageWidthModalEdit) { 1331 options.width = {}; 1332 } 1333 1334 t.openModalInsert(t.lang.insertImage, options, function (v) { // v are values 1335 t.execCmd('insertImage', v.url); 1336 var $img = $('img[src="' + v.url + '"]:not([alt])', t.$box); 1337 $img.attr('alt', v.alt); 1338 1339 if (t.o.imageWidthModalEdit) { 1340 $img.attr({ 1341 width: v.width 1342 }); 1343 } 1344 1345 t.syncCode(); 1346 t.$c.trigger('tbwchange'); 1347 1348 return true; 1349 }); 1350 }, 1351 fullscreen: function () { 1352 var t = this, 1353 prefix = t.o.prefix, 1354 fullscreenCssClass = prefix + 'fullscreen', 1355 isFullscreen; 1356 1357 t.$box.toggleClass(fullscreenCssClass); 1358 isFullscreen = t.$box.hasClass(fullscreenCssClass); 1359 $('body').toggleClass(prefix + 'body-fullscreen', isFullscreen); 1360 $(window).trigger('scroll'); 1361 t.$c.trigger('tbw' + (isFullscreen ? 'open' : 'close') + 'fullscreen'); 1362 }, 1363 1364 1365 /* 1366 * Call method of trumbowyg if exist 1367 * else try to call anonymous function 1368 * and finaly native execCommand 1369 */ 1370 execCmd: function (cmd, param, forceCss, skipTrumbowyg) { 1371 var t = this; 1372 skipTrumbowyg = !!skipTrumbowyg || ''; 1373 1374 if (cmd !== 'dropdown') { 1375 t.$ed.focus(); 1376 } 1377 1378 try { 1379 t.doc.execCommand('styleWithCSS', false, forceCss || false); 1380 } catch (c) { 1381 } 1382 1383 try { 1384 t[cmd + skipTrumbowyg](param); 1385 } catch (c) { 1386 try { 1387 cmd(param); 1388 } catch (e2) { 1389 if (cmd === 'insertHorizontalRule') { 1390 param = undefined; 1391 } else if (cmd === 'formatBlock' && t.isIE) { 1392 param = '<' + param + '>'; 1393 } 1394 1395 t.doc.execCommand(cmd, false, param); 1396 1397 t.syncCode(); 1398 t.semanticCode(false, true); 1399 } 1400 1401 if (cmd !== 'dropdown') { 1402 t.updateButtonPaneStatus(); 1403 t.$c.trigger('tbwchange'); 1404 } 1405 } 1406 }, 1407 1408 1409 // Open a modal box 1410 openModal: function (title, content) { 1411 var t = this, 1412 prefix = t.o.prefix; 1413 1414 // No open a modal box when exist other modal box 1415 if ($('.' + prefix + 'modal-box', t.$box).length > 0) { 1416 return false; 1417 } 1418 if (t.o.autogrowOnEnter) { 1419 t.autogrowOnEnterDontClose = true; 1420 } 1421 1422 t.saveRange(); 1423 t.showOverlay(); 1424 1425 // Disable all btnPane btns 1426 t.$btnPane.addClass(prefix + 'disable'); 1427 1428 // Build out of ModalBox, it's the mask for animations 1429 var $modal = $('<div/>', { 1430 class: prefix + 'modal ' + prefix + 'fixed-top' 1431 }).css({ 1432 top: t.$btnPane.height() 1433 }).appendTo(t.$box); 1434 1435 // Click on overlay close modal by cancelling them 1436 t.$overlay.one('click', function () { 1437 $modal.trigger(CANCEL_EVENT); 1438 return false; 1439 }); 1440 1441 // Build the form 1442 var $form = $('<form/>', { 1443 action: '', 1444 html: content 1445 }) 1446 .on('submit', function () { 1447 $modal.trigger(CONFIRM_EVENT); 1448 return false; 1449 }) 1450 .on('reset', function () { 1451 $modal.trigger(CANCEL_EVENT); 1452 return false; 1453 }) 1454 .on('submit reset', function () { 1455 if (t.o.autogrowOnEnter) { 1456 t.autogrowOnEnterDontClose = false; 1457 } 1458 }); 1459 1460 1461 // Build ModalBox and animate to show them 1462 var $box = $('<div/>', { 1463 class: prefix + 'modal-box', 1464 html: $form 1465 }) 1466 .css({ 1467 top: '-' + t.$btnPane.outerHeight() + 'px', 1468 opacity: 0 1469 }) 1470 .appendTo($modal) 1471 .animate({ 1472 top: 0, 1473 opacity: 1 1474 }, 100); 1475 1476 1477 // Append title 1478 $('<span/>', { 1479 text: title, 1480 class: prefix + 'modal-title' 1481 }).prependTo($box); 1482 1483 $modal.height($box.outerHeight() + 10); 1484 1485 1486 // Focus in modal box 1487 $('input:first', $box).focus(); 1488 1489 1490 // Append Confirm and Cancel buttons 1491 t.buildModalBtn('submit', $box); 1492 t.buildModalBtn('reset', $box); 1493 1494 1495 $(window).trigger('scroll'); 1496 1497 return $modal; 1498 }, 1499 // @param n is name of modal 1500 buildModalBtn: function (n, $modal) { 1501 var t = this, 1502 prefix = t.o.prefix; 1503 1504 return $('<button/>', { 1505 class: prefix + 'modal-button ' + prefix + 'modal-' + n, 1506 type: n, 1507 text: t.lang[n] || n 1508 }).appendTo($('form', $modal)); 1509 }, 1510 // close current modal box 1511 closeModal: function () { 1512 var t = this, 1513 prefix = t.o.prefix; 1514 1515 t.$btnPane.removeClass(prefix + 'disable'); 1516 t.$overlay.off(); 1517 1518 // Find the modal box 1519 var $modalBox = $('.' + prefix + 'modal-box', t.$box); 1520 1521 $modalBox.animate({ 1522 top: '-' + $modalBox.height() 1523 }, 100, function () { 1524 $modalBox.parent().remove(); 1525 t.hideOverlay(); 1526 }); 1527 1528 t.restoreRange(); 1529 }, 1530 // Preformated build and management modal 1531 openModalInsert: function (title, fields, cmd) { 1532 var t = this, 1533 prefix = t.o.prefix, 1534 lg = t.lang, 1535 html = ''; 1536 1537 $.each(fields, function (fieldName, field) { 1538 var l = field.label || fieldName, 1539 n = field.name || fieldName, 1540 a = field.attributes || {}; 1541 1542 var attr = Object.keys(a).map(function (prop) { 1543 return prop + '="' + a[prop] + '"'; 1544 }).join(' '); 1545 1546 html += '<label><input type="' + (field.type || 'text') + '" name="' + n + '"' + 1547 (field.type === 'checkbox' && field.value ? ' checked="checked"' : ' value="' + (field.value || '').replace(/"/g, '&quot;')) + 1548 '"' + attr + '><span class="' + prefix + 'input-infos"><span>' + 1549 (lg[l] ? lg[l] : l) + 1550 '</span></span></label>'; 1551 }); 1552 1553 return t.openModal(title, html) 1554 .on(CONFIRM_EVENT, function () { 1555 var $form = $('form', $(this)), 1556 valid = true, 1557 values = {}; 1558 1559 $.each(fields, function (fieldName, field) { 1560 var n = field.name || fieldName; 1561 1562 var $field = $('input[name="' + n + '"]', $form), 1563 inputType = $field.attr('type'); 1564 1565 switch (inputType.toLowerCase()) { 1566 case 'checkbox': 1567 values[n] = $field.is(':checked'); 1568 break; 1569 case 'radio': 1570 values[n] = $field.filter(':checked').val(); 1571 break; 1572 default: 1573 values[n] = $.trim($field.val()); 1574 break; 1575 } 1576 // Validate value 1577 if (field.required && values[n] === '') { 1578 valid = false; 1579 t.addErrorOnModalField($field, t.lang.required); 1580 } else if (field.pattern && !field.pattern.test(values[n])) { 1581 valid = false; 1582 t.addErrorOnModalField($field, field.patternError); 1583 } 1584 }); 1585 1586 if (valid) { 1587 t.restoreRange(); 1588 1589 if (cmd(values, fields)) { 1590 t.syncCode(); 1591 t.$c.trigger('tbwchange'); 1592 t.closeModal(); 1593 $(this).off(CONFIRM_EVENT); 1594 } 1595 } 1596 }) 1597 .one(CANCEL_EVENT, function () { 1598 $(this).off(CONFIRM_EVENT); 1599 t.closeModal(); 1600 }); 1601 }, 1602 addErrorOnModalField: function ($field, err) { 1603 var prefix = this.o.prefix, 1604 $label = $field.parent(); 1605 1606 $field 1607 .on('change keyup', function () { 1608 $label.removeClass(prefix + 'input-error'); 1609 }); 1610 1611 $label 1612 .addClass(prefix + 'input-error') 1613 .find('input+span') 1614 .append( 1615 $('<span/>', { 1616 class: prefix + 'msg-error', 1617 text: err 1618 }) 1619 ); 1620 }, 1621 1622 getDefaultImgDblClickHandler: function () { 1623 var t = this; 1624 1625 return function () { 1626 var $img = $(this), 1627 src = $img.attr('src'), 1628 base64 = '(Base64)'; 1629 1630 if (src.indexOf('data:image') === 0) { 1631 src = base64; 1632 } 1633 1634 var options = { 1635 url: { 1636 label: 'URL', 1637 value: src, 1638 required: true 1639 }, 1640 alt: { 1641 label: t.lang.description, 1642 value: $img.attr('alt') 1643 } 1644 }; 1645 1646 if (t.o.imageWidthModalEdit) { 1647 options.width = { 1648 value: $img.attr('width') ? $img.attr('width') : '' 1649 }; 1650 } 1651 1652 t.openModalInsert(t.lang.insertImage, options, function (v) { 1653 if (v.src !== base64) { 1654 $img.attr({ 1655 src: v.url 1656 }); 1657 } 1658 $img.attr({ 1659 alt: v.alt 1660 }); 1661 1662 if (t.o.imageWidthModalEdit) { 1663 if (parseInt(v.width) > 0) { 1664 $img.attr({ 1665 width: v.width 1666 }); 1667 } else { 1668 $img.removeAttr('width'); 1669 } 1670 } 1671 1672 return true; 1673 }); 1674 return false; 1675 }; 1676 }, 1677 1678 // Range management 1679 saveRange: function () { 1680 var t = this, 1681 documentSelection = t.doc.getSelection(); 1682 1683 t.range = null; 1684 1685 if (documentSelection.rangeCount) { 1686 var savedRange = t.range = documentSelection.getRangeAt(0), 1687 range = t.doc.createRange(), 1688 rangeStart; 1689 range.selectNodeContents(t.$ed[0]); 1690 range.setEnd(savedRange.startContainer, savedRange.startOffset); 1691 rangeStart = (range + '').length; 1692 t.metaRange = { 1693 start: rangeStart, 1694 end: rangeStart + (savedRange + '').length 1695 }; 1696 } 1697 }, 1698 restoreRange: function () { 1699 var t = this, 1700 metaRange = t.metaRange, 1701 savedRange = t.range, 1702 documentSelection = t.doc.getSelection(), 1703 range; 1704 1705 if (!savedRange) { 1706 return; 1707 } 1708 1709 if (metaRange && metaRange.start !== metaRange.end) { // Algorithm from http://jsfiddle.net/WeWy7/3/ 1710 var charIndex = 0, 1711 nodeStack = [t.$ed[0]], 1712 node, 1713 foundStart = false, 1714 stop = false; 1715 1716 range = t.doc.createRange(); 1717 1718 while (!stop && (node = nodeStack.pop())) { 1719 if (node.nodeType === 3) { 1720 var nextCharIndex = charIndex + node.length; 1721 if (!foundStart && metaRange.start >= charIndex && metaRange.start <= nextCharIndex) { 1722 range.setStart(node, metaRange.start - charIndex); 1723 foundStart = true; 1724 } 1725 if (foundStart && metaRange.end >= charIndex && metaRange.end <= nextCharIndex) { 1726 range.setEnd(node, metaRange.end - charIndex); 1727 stop = true; 1728 } 1729 charIndex = nextCharIndex; 1730 } else { 1731 var cn = node.childNodes, 1732 i = cn.length; 1733 1734 while (i > 0) { 1735 i -= 1; 1736 nodeStack.push(cn[i]); 1737 } 1738 } 1739 } 1740 } 1741 1742 documentSelection.removeAllRanges(); 1743 documentSelection.addRange(range || savedRange); 1744 }, 1745 getRangeText: function () { 1746 return this.range + ''; 1747 }, 1748 1749 updateButtonPaneStatus: function () { 1750 var t = this, 1751 prefix = t.o.prefix, 1752 tags = t.getTagsRecursive(t.doc.getSelection().focusNode), 1753 activeClasses = prefix + 'active-button ' + prefix + 'active'; 1754 1755 $('.' + prefix + 'active-button', t.$btnPane).removeClass(activeClasses); 1756 $.each(tags, function (i, tag) { 1757 var btnName = t.tagToButton[tag.toLowerCase()], 1758 $btn = $('.' + prefix + btnName + '-button', t.$btnPane); 1759 1760 if ($btn.length > 0) { 1761 $btn.addClass(activeClasses); 1762 } else { 1763 try { 1764 $btn = $('.' + prefix + 'dropdown .' + prefix + btnName + '-dropdown-button', t.$box); 1765 var dropdownBtnName = $btn.parent().data('dropdown'); 1766 $('.' + prefix + dropdownBtnName + '-button', t.$box).addClass(activeClasses); 1767 } catch (e) { 1768 } 1769 } 1770 }); 1771 }, 1772 getTagsRecursive: function (element, tags) { 1773 var t = this; 1774 tags = tags || (element && element.tagName ? [element.tagName] : []); 1775 1776 if (element && element.parentNode) { 1777 element = element.parentNode; 1778 } else { 1779 return tags; 1780 } 1781 1782 var tag = element.tagName; 1783 if (tag === 'DIV') { 1784 return tags; 1785 } 1786 if (tag === 'P' && element.style.textAlign !== '') { 1787 tags.push(element.style.textAlign); 1788 } 1789 1790 $.each(t.tagHandlers, function (i, tagHandler) { 1791 tags = tags.concat(tagHandler(element, t)); 1792 }); 1793 1794 tags.push(tag); 1795 1796 return t.getTagsRecursive(element, tags).filter(function (tag) { 1797 return tag != null; 1798 }); 1799 }, 1800 1801 // Plugins 1802 initPlugins: function () { 1803 var t = this; 1804 t.loadedPlugins = []; 1805 $.each($.trumbowyg.plugins, function (name, plugin) { 1806 if (!plugin.shouldInit || plugin.shouldInit(t)) { 1807 plugin.init(t); 1808 if (plugin.tagHandler) { 1809 t.tagHandlers.push(plugin.tagHandler); 1810 } 1811 t.loadedPlugins.push(plugin); 1812 } 1813 }); 1814 }, 1815 destroyPlugins: function () { 1816 $.each(this.loadedPlugins, function (i, plugin) { 1817 if (plugin.destroy) { 1818 plugin.destroy(); 1819 } 1820 }); 1821 } 1822 }; 1823 })(navigator, window, document, jQuery);
Download modules/editor/trumbowyg/trumbowyg.js
History Tue, 28 Aug 2018 00:33:27 +0200 Jan Dankert Editoren für Markdown (SimpleMDE) und HTML (Trumbowyg) installiert.