trumbowyg.min.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. /* ===========================================================
  2. * trumbowyg.js v1.0
  3. * Core code of Trumbowyg plugin
  4. * http://alex-d.github.com/Trumbowyg
  5. * ===========================================================
  6. * Author : Alexandre Demode (Alex-D)
  7. * Twitter : @AlexandreDemode
  8. * Website : alex-d.fr
  9. */
  10. $.trumbowyg = {
  11. langs: {
  12. en: {
  13. viewHTML: "Voir le HTML",
  14. formatting: "Format",
  15. p: "Paragraphe",
  16. blockquote: "Citation",
  17. code: "Code",
  18. header: "Titre",
  19. bold: "Gras",
  20. italic: "Italique",
  21. strikethrough: "Rayé",
  22. underline: "Souligné",
  23. strong: "Fort",
  24. em: "Emphase",
  25. del: "Supprimé",
  26. unorderedList: "Liste à puces",
  27. orderedList: "Liste ordonnée",
  28. insertImage: "Insérer une image",
  29. insertVideo: "Insérer une video",
  30. link: "Lien",
  31. createLink: "Insérer un lien",
  32. unlink: "Supprimer le lien",
  33. justifyLeft: "Aligner à gauche",
  34. justifyCenter: "Centrer",
  35. justifyRight: "Aligner à droite",
  36. justifyFull: "Justifier",
  37. horizontalRule: "Insérer un séparateur horizontal",
  38. fullscreen: "Plein écran",
  39. close: "Fermer",
  40. submit: "Valider",
  41. reset: "Annuler",
  42. invalidUrl: "URL invalide",
  43. required: "Obligatoire",
  44. description: "Description",
  45. title: "Title",
  46. text: "Text"
  47. }
  48. },
  49. // User default options
  50. opts: {},
  51. btnsGrps: {
  52. design: ['bold', 'italic', 'underline', 'strikethrough', 'upload'],
  53. semantic: ['strong', 'em', 'del'],
  54. justify: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
  55. lists: ['unorderedList', 'orderedList']
  56. }
  57. };
  58. (function($){
  59. $.fn.trumbowyg = function(opts, params){
  60. if($.isObject(opts) || opts == null){
  61. return this.each(function(){
  62. if(!$(this).data('trumbowyg'))
  63. $(this).data('trumbowyg', new Trumbowyg(this, opts));
  64. });
  65. } else if(this.length == 1){
  66. try {
  67. var t = $(this).data('trumbowyg');
  68. switch(opts){
  69. // Modal box
  70. case 'openModal':
  71. return t.openModal(params.title, params.content);
  72. case 'closeModal':
  73. return t.closeModal();
  74. case 'openModalInsert':
  75. return t.openModalInsert(params.title, params.fields, params.callback);
  76. // Selection
  77. case 'saveSelection':
  78. return t.saveSelection();
  79. case 'getSelection':
  80. return t.selection;
  81. case 'getSelectedText':
  82. return t.selection+'';
  83. case 'restoreSelection':
  84. return t.restoreSelection();
  85. // Destroy
  86. case 'destroy':
  87. return t.destroy();
  88. // Empty
  89. case 'empty':
  90. return t.empty();
  91. // Public options
  92. case 'lang':
  93. return t.lang;
  94. case 'duration':
  95. return t.o.duration;
  96. // HTML
  97. case 'html':
  98. return t.html(params);
  99. }
  100. } catch(e){}
  101. }
  102. return false;
  103. };
  104. var Trumbowyg = function(editorElem, opts){
  105. // jQuery object of the editor
  106. this.$e = $(editorElem);
  107. this.$creator = $(editorElem);
  108. // Extend with options
  109. opts = $.extend(true, {}, opts, $.trumbowyg.opts);
  110. // Localization management
  111. if(typeof opts.lang === 'undefined' || typeof $.trumbowyg.langs[opts.lang] === 'undefined')
  112. this.lang = $.trumbowyg.langs['en'];
  113. else
  114. this.lang = $.extend(true, {}, $.trumbowyg.langs['en'], $.trumbowyg.langs[opts.lang]);
  115. // Defaults Options
  116. this.o = $.extend(true, {
  117. lang: 'en',
  118. dir: 'ltr',
  119. duration: 200, // Duration of modal box animations
  120. mobile: false,
  121. tablet: true,
  122. closable: false,
  123. fullscreenable: true,
  124. fixedBtnPane: false,
  125. fixedFullWidth: false,
  126. semantic: false,
  127. resetCss: false,
  128. autogrow: false,
  129. prefix: 'trumbowyg-',
  130. convertLink: true,
  131. btns: [//'viewHTML',
  132. //'|',
  133. 'formatting',
  134. '|', $.trumbowyg.btnsGrps.design,
  135. '|', 'link',
  136. '|', $.trumbowyg.btnsGrps.justify,
  137. '|', $.trumbowyg.btnsGrps.lists,
  138. '|', 'horizontalRule'],
  139. btnsAdd: [],
  140. /**
  141. * When the button is associated to a empty object
  142. * func and title attributs are defined from the button key value
  143. *
  144. * For example
  145. * foo: {}
  146. * is equivalent to :
  147. * foo: {
  148. * func: 'foo',
  149. * title: this.lang.foo
  150. * }
  151. */
  152. btnsDef: {
  153. viewHTML: {
  154. func: 'toggle'
  155. },
  156. p: {
  157. func: 'formatBlock'
  158. },
  159. blockquote: {
  160. func: 'formatBlock'
  161. },
  162. h1: {
  163. func: 'formatBlock',
  164. title: this.lang.header + ' 1'
  165. },
  166. h2: {
  167. func: 'formatBlock',
  168. title: this.lang.header + ' 2'
  169. },
  170. h3: {
  171. func: 'formatBlock',
  172. title: this.lang.header + ' 3'
  173. },
  174. h4: {
  175. func: 'formatBlock',
  176. title: this.lang.header + ' 4'
  177. },
  178. bold: {},
  179. italic: {},
  180. underline: {},
  181. strikethrough: {},
  182. strong: {
  183. func: 'bold'
  184. },
  185. em: {
  186. func: 'italic'
  187. },
  188. del: {
  189. func: 'strikethrough'
  190. },
  191. createLink: {},
  192. unlink: {},
  193. insertImage: {},
  194. justifyLeft: {},
  195. justifyCenter: {},
  196. justifyRight: {},
  197. justifyFull: {},
  198. unorderedList: {
  199. func: 'insertUnorderedList'
  200. },
  201. orderedList: {
  202. func: 'insertOrderedList'
  203. },
  204. horizontalRule: {
  205. func: 'insertHorizontalRule'
  206. },
  207. // Dropdowns
  208. formatting: {
  209. dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4']
  210. },
  211. link: {
  212. dropdown: ['createLink', 'unlink']
  213. }
  214. }
  215. }, opts);
  216. if(this.o.semantic && !opts.btns)
  217. this.o.btns = [
  218. 'viewHTML',
  219. '|', 'formatting',
  220. '|', $.trumbowyg.btnsGrps.semantic,
  221. '|', 'link',
  222. '|', 'insertImage',
  223. '|', $.trumbowyg.btnsGrps.justify,
  224. '|', $.trumbowyg.btnsGrps.lists,
  225. '|', 'horizontalRule'
  226. ];
  227. else if(opts && opts.btns)
  228. this.o.btns = opts.btns;
  229. this.init();
  230. }
  231. Trumbowyg.prototype = {
  232. init: function(){
  233. this.height = this.$e.css('height');
  234. if(this.isEnabled()){
  235. this.buildEditor(true);
  236. return;
  237. }
  238. this.buildEditor();
  239. this.buildBtnPane();
  240. this.fixedBtnPaneEvents();
  241. this.buildOverlay();
  242. },
  243. buildEditor: function(disable){
  244. if(disable === true){
  245. if(!this.$e.is('textarea')){
  246. var textarea = this.buildTextarea().val(this.$e.val());
  247. this.$e.hide().after(textarea);
  248. }
  249. return;
  250. }
  251. this.$box = $('<div/>', {
  252. 'class': this.o.prefix + 'box ' + this.o.prefix + this.o.lang + ' trumbowyg'
  253. });
  254. this.isTextarea = true;
  255. if(this.$e.is('textarea'))
  256. this.$editor = $('<div/>');
  257. else {
  258. this.$editor = this.$e;
  259. this.$e = this.buildTextarea().val(this.$e.val());
  260. this.isTextarea = false;
  261. }
  262. this.$e.hide()
  263. .addClass(this.o.prefix + 'textarea');
  264. var html = '';
  265. if(this.isTextarea){
  266. html = this.$e.val();
  267. this.$box.insertAfter(this.$e)
  268. .append(this.$editor)
  269. .append(this.$e);
  270. } else {
  271. html = this.$editor.html();
  272. this.$box.insertAfter(this.$editor)
  273. .append(this.$e)
  274. .append(this.$editor);
  275. this.syncCode();
  276. }
  277. this.$editor.addClass(this.o.prefix + 'editor')
  278. .attr('contenteditable', true)
  279. .attr('dir', this.o.dir)
  280. .html(html);
  281. if(this.o.resetCss)
  282. this.$editor.addClass(this.o.prefix + 'reset-css');
  283. if(!this.o.autogrow){
  284. $.each([this.$editor, this.$e], $.proxy(function(i, $el){
  285. $el.css({
  286. height: this.height,
  287. overflow: 'auto'
  288. });
  289. }, this));
  290. }
  291. if(this.o.semantic){
  292. this.$editor.html(
  293. this.$editor.html()
  294. .replace("<br>", "</p><p>")
  295. .replace("&nsbp;", "")
  296. );
  297. this.semanticCode();
  298. }
  299. var that = this;
  300. this.$editor
  301. .on('dblclick', 'img', function(){
  302. var $img = $(this);
  303. that.openModalInsert(that.lang.insertImage, {
  304. url: {
  305. label: 'URL',
  306. value: $img.attr('src'),
  307. required: true
  308. },
  309. alt: {
  310. label: 'description',
  311. value: $img.attr('alt')
  312. }
  313. }, function(values){
  314. $img.attr('src', values['url']);
  315. $img.attr('alt', values['alt']);
  316. });
  317. return false;
  318. })
  319. .on('keyup', function(e){
  320. that.semanticCode(false, e.which === 13);
  321. })
  322. .on('blur', function(){
  323. that.syncCode();
  324. });
  325. },
  326. // Build the Textarea which contain HTML generated code
  327. buildTextarea: function(){
  328. return $('<textarea/>', {
  329. 'name': this.$e.attr('id'),
  330. 'height': this.height
  331. });
  332. },
  333. // Build button pane, use o.btns and o.btnsAdd options
  334. buildBtnPane: function(){
  335. var t = this;
  336. if(t.o.btns === false) return;
  337. var pfx = t.o.prefix;
  338. t.$btnPane = $('<ul/>', {
  339. 'class': pfx + 'button-pane'
  340. });
  341. $.each(t.o.btns.concat(t.o.btnsAdd), $.proxy(function(i, btn){
  342. // Managment of group of buttons
  343. try {
  344. var b = btn.split('btnGrp-');
  345. if(b[1] != undefined)
  346. btn = $.trumbowyg.btnsGrps[b[1]];
  347. } catch(e){}
  348. if(!$.isArray(btn)) btn = [btn];
  349. $.each(btn, $.proxy(function(i, btn){
  350. try { // Prevent buildBtn error
  351. var $li = $('<li/>');
  352. if(btn === '|') // It's a separator
  353. $li.addClass(pfx + 'separator');
  354. else if(t.isSupportedBtn(btn)){ // It's a supported button
  355. if(btn === 'viewHTML')
  356. $li.addClass(pfx + 'not-disable');
  357. $li.append(t.buildBtn(btn));
  358. }
  359. t.$btnPane.append($li);
  360. } catch(e){}
  361. }, t));
  362. }, t));
  363. // Build right li for fullscreen and close buttons
  364. var $liRight = $('<li/>', {
  365. 'class': pfx + 'not-disable ' + pfx + 'buttons-right'
  366. });
  367. // Add the fullscreen button
  368. if(t.o.fullscreenable)
  369. $liRight
  370. .append(t.buildRightBtn('fullscreen')
  371. .on('click', $.proxy(function(e){
  372. var cssClass = pfx + 'fullscreen';
  373. t.$box.toggleClass(cssClass);
  374. if(t.$box.hasClass(cssClass)){
  375. $('body').css('overflow', 'hidden');
  376. t.$box.css({
  377. position: 'fixed',
  378. top: 0,
  379. left: 0,
  380. width: '100%',
  381. height: '100%',
  382. margin: 0,
  383. padding: 0,
  384. zIndex: 99999
  385. });
  386. $([t.$editor, t.$e]).each(function(){
  387. $(this).css({
  388. height: '100%',
  389. overflow: 'auto'
  390. });
  391. });
  392. t.$btnPane.css('width', '100%');
  393. } else {
  394. $('body').css('overflow', 'auto');
  395. t.$box.removeAttr('style');
  396. if(!t.o.autogrow){
  397. h = t.height;
  398. $([t.$editor, t.$e]).each(function(i, $el){
  399. $el.css('height', h);
  400. });
  401. }
  402. }
  403. $(window).trigger('scroll');
  404. }, t)));
  405. // Build and add close button
  406. if(t.o.closable)
  407. $liRight
  408. .append(t.buildRightBtn('close')
  409. .on('click', $.proxy(function(e){
  410. var cssClass = pfx + 'fullscreen';
  411. if(t.$box.hasClass(cssClass))
  412. $('body').css('overflow', 'auto');
  413. t.destroy();
  414. }, t)));
  415. // Add right li only if isn't empty
  416. if($liRight.not(':empty'))
  417. t.$btnPane.append($liRight);
  418. t.$box.prepend(t.$btnPane);
  419. },
  420. // Build a button and his action
  421. buildBtn: function(name){
  422. var pfx = this.o.prefix,
  423. btnDef = this.o.btnsDef[name],
  424. t = this,
  425. textDef = this.lang[name] || name.charAt(0).toUpperCase() + name.slice(1);
  426. var $btn = $('<button/>', {
  427. 'type': 'button',
  428. 'class': pfx + name +'-button' + (btnDef.ico ? ' '+ pfx + btnDef.ico +'-button' : ''),
  429. 'text': btnDef.text || btnDef.title || textDef,
  430. 'title': btnDef.title || btnDef.text || textDef,
  431. 'mousedown': function(e){
  432. if(!btnDef.dropdown || t.$box.find('.'+name+'-'+pfx + 'dropdown').is(':hidden'))
  433. $('body').trigger('mousedown');
  434. if(t.$btnPane.hasClass(pfx + 'disable')
  435. && !$(this).parent().hasClass(pfx + 'not-disable'))
  436. return false;
  437. t.execCommand((btnDef.dropdown ? 'dropdown' : false) || btnDef.func || name,
  438. btnDef.param || name);
  439. e.stopPropagation();
  440. e.preventDefault();
  441. return false;
  442. }
  443. });
  444. if(btnDef.dropdown){
  445. $btn.addClass(pfx + 'open-dropdown');
  446. var cssClass = pfx + 'dropdown';
  447. var dropdown = $('<div/>', {
  448. 'class': name + '-' + cssClass + ' ' + cssClass + ' ' + pfx + 'fixed-top'
  449. });
  450. for(var i = 0, c = btnDef.dropdown.length; i < c; i++){
  451. if(t.o.btnsDef[btnDef.dropdown[i]] && t.isSupportedBtn(btnDef.dropdown[i]))
  452. dropdown.append(t.buildSubBtn(btnDef.dropdown[i]));
  453. }
  454. this.$box.append(dropdown.hide());
  455. }
  456. return $btn;
  457. },
  458. // Build a button for dropdown menu
  459. buildSubBtn: function(name){
  460. var btnDef = this.o.btnsDef[name];
  461. return $('<button/>', {
  462. 'type': 'button',
  463. 'text': btnDef.text || btnDef.title || this.lang[name] || name,
  464. 'mousedown': $.proxy(function(e){
  465. $('body').trigger('mousedown');
  466. this.execCommand(btnDef.func || name,
  467. btnDef.param || name);
  468. e.stopPropagation();
  469. e.preventDefault();
  470. return false;
  471. }, this)
  472. });
  473. },
  474. // Build a button for right li
  475. buildRightBtn: function(name){
  476. return $('<button/>', {
  477. 'type': 'button',
  478. 'class': this.o.prefix + name+'-button',
  479. 'title': this.lang[name],
  480. 'text': this.lang[name]
  481. });
  482. },
  483. // Check if button is supported
  484. isSupportedBtn: function(btn){
  485. return typeof this.o.btnsDef[btn].isSupported !== 'function' || this.o.btnsDef[btn].isSupported()
  486. },
  487. // Build overlay for modal box
  488. buildOverlay: function(){
  489. return this.$overlay = $('<div/>', {
  490. 'class': this.o.prefix + 'overlay'
  491. }).css({
  492. top: this.$btnPane.outerHeight(),
  493. height: (parseInt(this.$editor.outerHeight()) + 1) + 'px'
  494. }).appendTo(this.$box);
  495. },
  496. showOverlay: function(){
  497. $(window).trigger('scroll');
  498. this.$overlay.fadeIn(this.o.duration);
  499. this.$box.addClass(this.o.prefix + 'box-blur');
  500. },
  501. hideOverlay: function(){
  502. this.$overlay.fadeOut(this.o.duration/4);
  503. this.$box.removeClass(this.o.prefix + 'box-blur');
  504. },
  505. // Management of fixed button pane
  506. fixedBtnPaneEvents: function(){
  507. if(!this.o.fixedBtnPane)
  508. return;
  509. this.isFixed = false;
  510. $(window).on('scroll', $.proxy(function(e){
  511. if(!this.$box)
  512. return;
  513. this.syncCode();
  514. var wScroll = $(window).scrollTop(),
  515. offset = this.$box.offset().top + 1,
  516. toFixed = (wScroll - offset > 0) && ((wScroll - offset - parseInt(this.height)) < 0);
  517. if(toFixed){
  518. if(!this.isFixed){
  519. this.isFixed = true;
  520. this.$btnPane.css({
  521. position: 'fixed',
  522. top: 0,
  523. left: (this.o.fixedFullWidth) ? '0' : 'auto',
  524. width: (this.o.fixedFullWidth) ? '100%' : ((parseInt(this.$box.css('width'))-1) + 'px'),
  525. zIndex: 7
  526. });
  527. this.$editor.css({ marginTop: this.$btnPane.css('height') });
  528. this.$e.css({ marginTop: this.$btnPane.css('height') });
  529. }
  530. $('.' + this.o.prefix + 'fixed-top', this.$box).css({
  531. position: this.o.fixedFullWidth ? 'fixed' : 'absolute',
  532. top: this.o.fixedFullWidth ? this.$btnPane.outerHeight() : parseInt(this.$btnPane.outerHeight()) + (wScroll - offset) + 'px',
  533. zIndex: 15
  534. });
  535. } else if(this.isFixed) {
  536. this.isFixed = false;
  537. this.$btnPane.css({ position: 'relative' });
  538. this.$editor.css({ marginTop: 0 });
  539. this.$e.css({ marginTop: 0 });
  540. $('.' + this.o.prefix + 'fixed-top', this.$box).css({
  541. position: 'absolute',
  542. top: this.$btnPane.outerHeight()
  543. });
  544. }
  545. }, this));
  546. },
  547. // Destroy the editor
  548. destroy: function(){
  549. var html = this.html();
  550. if(this.isTextarea)
  551. this.$box.after(this.$e.css({height: this.height})
  552. .val(html)
  553. .removeClass(this.o.prefix + 'textarea')
  554. .show());
  555. else
  556. this.$box.after(this.$editor.css({height: this.height})
  557. .removeClass(this.o.prefix + 'editor')
  558. .attr('contenteditable', false)
  559. .html(html)
  560. .show());
  561. this.$box.remove();
  562. this.$creator.removeData('trumbowyg');
  563. },
  564. // Empty the editor
  565. empty: function(){
  566. this.$e.val('');
  567. this.syncCode(true);
  568. },
  569. // Function call when click on viewHTML button
  570. toggle: function(){
  571. this.semanticCode(false, true);
  572. this.$editor.toggle();
  573. this.$e.toggle();
  574. this.$btnPane.toggleClass(this.o.prefix + 'disable');
  575. this.$btnPane.find('.'+this.o.prefix + 'viewHTML-button').toggleClass(this.o.prefix + 'active');
  576. },
  577. // Open dropdown when click on a button which open that
  578. dropdown: function(name){
  579. var pfx = this.o.prefix;
  580. var $dropdown = this.$box.find('.'+name+'-'+pfx + 'dropdown'),
  581. $btn = this.$btnPane.find('.'+pfx+name+'-button');
  582. if($dropdown.is(':hidden')){
  583. $btn.addClass(this.o.prefix + 'active');
  584. $dropdown.css({
  585. position: 'absolute',
  586. top: this.$btnPane.outerHeight(),
  587. left: (this.o.fixedFullWidth && this.isFixed) ? $btn.offset().left+'px' : ($btn.offset().left - this.$btnPane.offset().left)+'px'
  588. }).show();
  589. $(window).trigger('scroll');
  590. $('body').on('mousedown', $.proxy(function(e){
  591. $('.' + pfx + 'dropdown').hide();
  592. $('.' + pfx + 'active').removeClass(pfx + 'active');
  593. $('body').off('mousedown');
  594. }, this));
  595. } else {
  596. $('body').trigger('mousedown');
  597. }
  598. },
  599. // HTML Code management
  600. html: function(html){
  601. if(html){
  602. this.$e.val(html);
  603. this.syncCode(true);
  604. return tbw;
  605. } else
  606. return this.$e.val();
  607. },
  608. syncCode: function(force){
  609. if(!force && this.$editor.is(':visible'))
  610. this.$e.val(this.$editor.html());
  611. else
  612. this.$editor.html(this.$e.val());
  613. if(this.o.autogrow){
  614. this.height = this.$editor.css('height');
  615. this.$e.css({ height: this.height });
  616. }
  617. },
  618. // Analyse and update to semantic code
  619. // @param force : force to sync code from textarea
  620. // @param full : wrap text nodes in <p>
  621. semanticCode: function(force, full){
  622. this.syncCode(force);
  623. if(this.o.semantic){
  624. this.semanticTag('b', 'strong');
  625. this.semanticTag('i', 'em');
  626. this.semanticTag('strike', 'del');
  627. if(full){
  628. // Wrap text nodes in p
  629. this.$editor.contents()
  630. .filter(function(){
  631. // Only non-empty text nodes
  632. return this.nodeType === 3 && $.trim(this.nodeValue).length > 0;
  633. }).wrap('<p></p>').end()
  634. // Remove all br
  635. .filter("br").remove();
  636. this.saveSelection();
  637. this.semanticTag('div', 'p');
  638. this.restoreSelection();
  639. }
  640. this.$e.val(this.$editor.html());
  641. }
  642. },
  643. semanticTag: function(oldTag, newTag){
  644. $(oldTag, this.$editor).each(function(){
  645. $(this).replaceWith(function(){ return '<'+newTag+'>' + $(this).html() + '</'+newTag+'>'; });
  646. });
  647. },
  648. // Function call when user click on « Insert Link »
  649. createLink: function(){
  650. var that = this;
  651. this.saveSelection();
  652. this.openModalInsert(this.lang.createLink, {
  653. url: {
  654. label: 'URL',
  655. value: 'http://',
  656. required: true
  657. },
  658. title: {
  659. label: this.lang.title,
  660. value: this.selection
  661. },
  662. text: {
  663. label: this.lang.text,
  664. value: this.selection
  665. }
  666. }, function(values){
  667. that.execCommand('createLink', values['url']);
  668. var link = $(['a[href="', values['url'], '"]:not([title])'].join(''), that.$box);
  669. if($.trim(values['text']).length !== 0)
  670. link.text(values['text']);
  671. if($.trim(values['title']).length !== 0)
  672. link.attr('title', values['title']);
  673. return true;
  674. });
  675. },
  676. insertImage: function(){
  677. var that = this;
  678. this.saveSelection();
  679. this.openModalInsert(this.lang.insertImage, {
  680. url: {
  681. label: 'URL',
  682. value: 'http://',
  683. required: true
  684. },
  685. alt: {
  686. label: 'description',
  687. value: this.selection
  688. }
  689. }, function(values){
  690. that.execCommand('insertImage', values['url']);
  691. $(['img[src="', values['url'], '"]:not([alt])'].join(''), that.$box).attr('alt', values['alt']);
  692. return true;
  693. });
  694. },
  695. /*
  696. * Call method of trumbowyg if exist
  697. * else try to call anonymous function
  698. * and finaly native execCommand
  699. */
  700. execCommand: function(cmd, param){
  701. if(cmd != 'dropdown')
  702. this.$editor.focus();
  703. try {
  704. this[cmd](param);
  705. } catch(e){
  706. try {
  707. cmd(param, this);
  708. } catch(e){
  709. this.$editor.focus();
  710. if(cmd == 'insertHorizontalRule')
  711. param = null;
  712. document.execCommand(cmd, false, param);
  713. }
  714. }
  715. this.syncCode();
  716. },
  717. formatBlock: function(param){
  718. if($.browser.msie)
  719. param = '<' + param + '>';
  720. document.execCommand('formatBlock', false, param);
  721. },
  722. // Open a modal box
  723. openModal: function(title, content){
  724. var pfx = this.o.prefix;
  725. // No open a modal box when exist other modal box
  726. if($('.' + pfx + 'modal-box', this.$box).size() > 0)
  727. return false;
  728. this.saveSelection();
  729. this.showOverlay();
  730. // Disable all btnPane btns
  731. this.$btnPane.addClass(pfx + 'disable');
  732. $('.' + pfx + 'not-disable', this.$btnPane)
  733. .not('.' + pfx + 'buttons-right')
  734. .removeClass(pfx + 'not-disable')
  735. .addClass(pfx + 'not-disable-old');
  736. // Build out of ModalBox, it's the mask for animations
  737. var $modal = $('<div/>', {
  738. 'class': pfx + 'modal ' + pfx + 'fixed-top'
  739. }).css({
  740. top: (parseInt(this.$btnPane.css('height')) + 1) + 'px'
  741. }).appendTo(this.$box);
  742. // Click on overflay close modal by cancelling them
  743. this.$overlay.one('click', function(e){
  744. e.preventDefault();
  745. $modal.trigger(pfx + 'cancel');
  746. });
  747. $e = this.$editor;
  748. // Build the form
  749. $form = $('<form/>', {
  750. action: 'javascript:void(null);',
  751. html: content
  752. })
  753. .on('submit', function(e){
  754. e.preventDefault();
  755. $modal.trigger(pfx + 'confirm');
  756. })
  757. .on('reset', function(e){
  758. e.preventDefault();
  759. $modal.trigger(pfx + 'cancel');
  760. });
  761. // Build ModalBox and animate to show them
  762. var $modalBox = $('<div/>', {
  763. 'class': pfx + 'modal-box',
  764. html: $form
  765. })
  766. .css({
  767. top: '-' + parseInt(this.$btnPane.outerHeight()) + 'px',
  768. opacity: 0
  769. })
  770. .appendTo($modal)
  771. .animate({
  772. top: 0,
  773. opacity: 1
  774. }, this.o.duration / 2);
  775. // Append title
  776. $('<span/>', {
  777. text: title,
  778. 'class': pfx + 'modal-title'
  779. }).prependTo($modalBox);
  780. // Focus in modal box
  781. $modalBox.find('input:first').focus();
  782. // Append Confirm and Cancel buttons
  783. this.buildModalBtn('submit', $modalBox);
  784. this.buildModalBtn('reset', $modalBox);
  785. $('body').trigger('scroll');
  786. return $modal;
  787. },
  788. buildModalBtn: function(name, modal){
  789. return $('<button/>', {
  790. 'class': this.o.prefix + 'modal-button ' + this.o.prefix + 'modal-' + name,
  791. 'type': name,
  792. 'text': this.lang[name] || name
  793. }).appendTo(modal.find('form'));
  794. },
  795. // close current modal box
  796. closeModal: function(){
  797. var pfx = this.o.prefix;
  798. this.$btnPane.removeClass(pfx + 'disable');
  799. this.$overlay.off();
  800. $('.' + this.o.prefix + 'not-disable-old', this.$btnPane)
  801. .removeClass(pfx + 'not-disable-old')
  802. .addClass(pfx + 'not-disable');
  803. var that = this,
  804. $modalBox = $('.' + pfx + 'modal-box', this.$box);
  805. $modalBox.animate({
  806. top: '-' + $modalBox.css('height')
  807. }, this.o.duration/2, function(){
  808. $(this).parent().remove();
  809. that.hideOverlay();
  810. });
  811. },
  812. // Preformated build and management modal
  813. openModalInsert: function(title, fields, cmd){
  814. var html = '',
  815. pfx = this.o.prefix;
  816. for(f in fields){
  817. var fd = fields[f];
  818. label = (fd.label == undefined)
  819. ? (this.lang[f] ? this.lang[f] : f.charAt(0).toUpperCase() + f.slice(1))
  820. : (this.lang[fd.label] ? this.lang[fd.label] : fd.label)
  821. ;
  822. if(fd.name == undefined)
  823. fd.name = f;
  824. if(!fd.pattern && f == 'url'){
  825. fd.pattern = /^(http|https):\/\/([\w~#!:.?+=&%@!\-\/]+)$/;
  826. fd.patternError = this.lang.invalidUrl;
  827. }
  828. html += '<label><input type="'+(fd.type || 'text')+'" name="'+fd.name+'" value="'+(fd.value || '')+'"><span class="'+pfx+'input-infos"><span>'+label+'</span></span></label>';
  829. }
  830. var modBox = this.openModal(title, html),
  831. that = this;
  832. modBox
  833. .on(pfx + 'confirm', function(){
  834. var $form = $(this).find('form'),
  835. valid = true,
  836. values = {};
  837. for(f in fields) {
  838. var $field = $('input[name="'+f+'"]', $form);
  839. values[f] = $field.val();
  840. // Validate value
  841. if(fields[f].required && (values[f] == null || values[f] == undefined || $.trim(values[f]) == "")) {
  842. valid = false;
  843. that.addErrorOnModalField($field, that.lang.required);
  844. } else if(fields[f].pattern && !fields[f].pattern.test(values[f])) {
  845. valid = false;
  846. that.addErrorOnModalField($field, fields[f].patternError);
  847. }
  848. }
  849. if(valid) {
  850. that.restoreSelection();
  851. if(cmd(values, fields)) {
  852. that.syncCode();
  853. that.closeModal();
  854. modBox.off(pfx + 'confirm');
  855. }
  856. }
  857. })
  858. .one(pfx + 'cancel', function(){
  859. modBox.off(pfx + 'confirm');
  860. that.closeModal();
  861. that.restoreSelection();
  862. });
  863. return modBox;
  864. },
  865. addErrorOnModalField: function($field, err){
  866. var $label = $field.parent(),
  867. pfx = this.o.prefix;
  868. $label.addClass(pfx + 'input-error');
  869. $field.on('change keyup', function(){ $label.removeClass(pfx + 'input-error'); });
  870. $label.find('input+span').append('<span class="'+ pfx +'msg-error">'+ err +'</span>');
  871. },
  872. // Selection management
  873. saveSelection: function(){
  874. this.selection = null;
  875. if(window.getSelection){
  876. var sel = window.getSelection();
  877. if(sel.getRangeAt && sel.rangeCount)
  878. this.selection = sel.getRangeAt(0);
  879. } else if(document.selection && document.selection.createRange){
  880. this.selection = document.selection.createRange();
  881. }
  882. },
  883. restoreSelection: function(){
  884. range = this.selection;
  885. if(range){
  886. if(window.getSelection){
  887. var sel = window.getSelection();
  888. sel.removeAllRanges();
  889. sel.addRange(range);
  890. } else if(document.selection && range.select){
  891. range.select();
  892. }
  893. }
  894. },
  895. // Return true if must enable Trumbowyg on this mobile device
  896. isEnabled: function(){
  897. var mobile = "iPhone|iPod|Android|BlackBerry|Windows\sPhone|ZuneWP7";
  898. var exprTablet = new RegExp("(iPad|webOS)");
  899. var exprMobile = new RegExp("("+mobile+")");
  900. return (this.o.tablet === true && exprTablet.test(navigator.userAgent))
  901. || (this.o.mobile === true && exprMobile.test(navigator.userAgent));
  902. }
  903. };
  904. /* isObject */
  905. var toString = Object.prototype.toString, hasOwnProp = Object.prototype.hasOwnProperty;
  906. $.isObject = function(obj) { if(toString.call(obj) !== "[object Object]") return false; var key; for(key in obj){} return !key || hasOwnProp.call(obj, key); };
  907. $.isString = function(str){ return typeof(str) === 'string' };
  908. })(jQuery);
  909. /* ===========================================================
  910. * trumbowyg.upload.js v1.0
  911. * Upload plugin for Trumbowyg
  912. * http://alex-d.github.com/Trumbowyg
  913. * ===========================================================
  914. * Author : Alexandre Demode (Alex-D)
  915. * Twitter : @AlexandreDemode
  916. * Website : alex-d.fr
  917. */
  918. (function($){
  919. addXhrProgressEvent();
  920. $.extend(true, $.trumbowyg, {
  921. langs: {
  922. en: {
  923. upload: "Envoi d'un fichier",
  924. file: "Fichier",
  925. uploadError: "Erreur"
  926. }
  927. },
  928. upload: {
  929. serverPath: './trumbowyg.upload.php'
  930. },
  931. opts: {
  932. btnsDef: {
  933. upload: {
  934. func: function(params, tbw){
  935. var file,
  936. pfx = tbw.o.prefix;
  937. var $modal = tbw.openModalInsert(
  938. // Title
  939. tbw.lang['upload'],
  940. // Fields
  941. {
  942. file: {
  943. type: 'file',
  944. required: true
  945. }
  946. },
  947. // Callback
  948. function(values, fields){
  949. var data = new FormData();
  950. data.append('file', file);
  951. if($('.' + pfx +'progress', $modal).length == 0)
  952. $('.' + pfx + 'modal-title', $modal)
  953. .after(
  954. $('<div/>', {
  955. 'class': pfx +'progress'
  956. })
  957. .append(
  958. $('<div/>', {
  959. 'class': pfx +'progress-bar'
  960. })
  961. )
  962. );
  963. $.ajax({
  964. url: $.trumbowyg.upload.serverPath,
  965. type: 'POST',
  966. data: data,
  967. cache: false,
  968. dataType: 'json',
  969. processData: false,
  970. contentType: false,
  971. progressUpload: function(e){
  972. $('.' + pfx + 'progress-bar').stop().animate({
  973. width: Math.round(e.loaded * 100 / e.total) + '%'
  974. }, 200);
  975. },
  976. success: function(data){
  977. if(data.error.length == 0) {
  978. if(data.file.ext == 'jpg' || data.file.ext == 'jpeg' || data.file.ext == 'gif' || data.file.ext == 'png' || data.file.ext == 'bmp'){
  979. tbw.execCommand('insertImage', data.file.url);
  980. }else{
  981. tbw.execCommand("InsertHTML", '<a href="'+data.file.url+'">'+data.file.name+'</a>');
  982. }
  983. setTimeout(function(){
  984. tbw.closeModal();
  985. }, 250);
  986. } else {
  987. alert(data.error.join('\n-'));
  988. tbw.addErrorOnModalField(
  989. $('input[type=file]', $modal),'Erreur'
  990. );
  991. }
  992. },
  993. error: function(data){
  994. tbw.addErrorOnModalField(
  995. $('input[type=file]', $modal),
  996. tbw.lang['uploadError']
  997. );
  998. }
  999. });
  1000. }
  1001. );
  1002. $('input[type=file]').on('change', function(e){
  1003. file = e.target.files[0];
  1004. });
  1005. }
  1006. }
  1007. }
  1008. }
  1009. });
  1010. function addXhrProgressEvent(){
  1011. var originalXhr = $.ajaxSettings.xhr;
  1012. $.ajaxSetup({
  1013. xhr: function() {
  1014. var req = originalXhr(),
  1015. that = this;
  1016. if(req && typeof req.upload == "object" && that.progressUpload !== undefined)
  1017. req.upload.addEventListener("progress", function(e){
  1018. that.progressUpload(e);
  1019. }, false);
  1020. return req;
  1021. }
  1022. });
  1023. }
  1024. })(jQuery);