PluginProbe
Page Builder: Pagelayer – Drag and Drop website builder / 2.1.6
Page Builder: Pagelayer – Drag and Drop website builder v2.1.6
2.2.1 2.2.0 2.1.9 2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 trunk 0.9.0 0.9.1 0.9.2 0.9.3 0.9.4 0.9.5 0.9.6 0.9.7 0.9.8 0.9.9 1.0.0 1.0.2 1.0.3 1.0.4 1.0.5 All 129 releases
pagelayer / js / trumbowyg.js

trumbowyg.js in Page Builder: Pagelayer – Drag and Drop website builder 2.1.6, at js/trumbowyg.js

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