scratch – Blame information for rev 58

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