github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/pubnot/trumbowyg/trumbowyg.js (about)

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