scratch – Blame information for rev 125

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