openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

trumbowyg.js (61762B)


      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);