Merge branch '2.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / 3rdParty / redactor / redactor.js
1 /*
2 Redactor v9.2.1
3 Updated: Mar 19, 2014
4
5 http://imperavi.com/redactor/
6
7 Copyright (c) 2009-2014, Imperavi LLC.
8 License: http://imperavi.com/redactor/license/
9
10 Usage: $('#content').redactor();
11 */
12
13 (function($)
14 {
15 var uuid = 0;
16
17 "use strict";
18
19 var Range = function(range)
20 {
21 this[0] = range.startOffset;
22 this[1] = range.endOffset;
23
24 this.range = range;
25
26 return this;
27 };
28
29 Range.prototype.equals = function()
30 {
31 return this[0] === this[1];
32 };
33
34 // Plugin
35 $.fn.redactor = function(options)
36 {
37 var val = [];
38 var args = Array.prototype.slice.call(arguments, 1);
39
40 if (typeof options === 'string')
41 {
42 this.each(function()
43 {
44 var instance = $.data(this, 'redactor');
45 if (typeof instance !== 'undefined' && $.isFunction(instance[options]))
46 {
47 var methodVal = instance[options].apply(instance, args);
48 if (methodVal !== undefined && methodVal !== instance) val.push(methodVal);
49 }
50 else return $.error('No such method "' + options + '" for Redactor');
51 });
52 }
53 else
54 {
55 this.each(function()
56 {
57 if (!$.data(this, 'redactor')) $.data(this, 'redactor', Redactor(this, options));
58 });
59 }
60
61 if (val.length === 0) return this;
62 else if (val.length === 1) return val[0];
63 else return val;
64
65 };
66
67 // Initialization
68 function Redactor(el, options)
69 {
70 return new Redactor.prototype.init(el, options);
71 }
72
73 $.Redactor = Redactor;
74 $.Redactor.VERSION = '9.2.1';
75 $.Redactor.opts = {
76
77 // settings
78 rangy: false,
79
80 iframe: false,
81 fullpage: false,
82 css: false, // url
83
84 lang: 'en',
85 direction: 'ltr', // ltr or rtl
86
87 placeholder: '',
88
89 typewriter: false,
90 wym: false,
91 mobile: true,
92 cleanup: true,
93 tidyHtml: true,
94 pastePlainText: false,
95 removeEmptyTags: true,
96 cleanSpaces: true,
97 cleanFontTag: true,
98 templateVars: false,
99 xhtml: false,
100
101 visual: true,
102 focus: false,
103 tabindex: false,
104 autoresize: true,
105 minHeight: false,
106 maxHeight: false,
107 shortcuts: true,
108
109 autosave: false, // false or url
110 autosaveInterval: 60, // seconds
111
112 plugins: false, // array
113
114 //linkAnchor: true,
115 //linkEmail: true,
116 linkProtocol: 'http://',
117 linkNofollow: false,
118 linkSize: 50,
119
120 imageFloatMargin: '10px',
121 imageGetJson: false, // url (ex. /folder/images.json ) or false
122
123 dragUpload: true, // false
124 imageTabLink: true,
125 imageUpload: false, // url
126 imageUploadParam: 'file', // input name
127
128 fileUpload: false, // url
129 fileUploadParam: 'file', // input name
130 clipboardUpload: true, // or false
131 clipboardUploadUrl: false, // url
132
133 dnbImageTypes: ['image/png', 'image/jpeg', 'image/gif'], // or false
134
135 s3: false,
136 uploadFields: false,
137
138 observeImages: true,
139 observeLinks: true,
140
141 modalOverlay: true,
142
143 tabSpaces: false, // true or number of spaces
144 tabFocus: true,
145
146 air: false,
147 airButtons: ['formatting', 'bold', 'italic', 'deleted', 'unorderedlist', 'orderedlist', 'outdent', 'indent'],
148
149 toolbar: true,
150 toolbarFixed: false,
151 toolbarFixedTarget: document,
152 toolbarFixedTopOffset: 0, // pixels
153 toolbarFixedBox: false,
154 toolbarExternal: false, // ID selector
155 toolbarOverflow: false,
156 buttonSource: true,
157
158 buttons: ['html', 'formatting', 'bold', 'italic', 'deleted', 'unorderedlist', 'orderedlist',
159 'outdent', 'indent', 'image', 'video', 'file', 'table', 'link', 'alignment', '|',
160 'horizontalrule'], // 'underline', 'alignleft', 'aligncenter', 'alignright', 'justify'
161 buttonsHideOnMobile: [],
162
163 activeButtons: ['deleted', 'italic', 'bold', 'underline', 'unorderedlist', 'orderedlist',
164 'alignleft', 'aligncenter', 'alignright', 'justify', 'table'],
165 activeButtonsStates: {
166 b: 'bold',
167 strong: 'bold',
168 i: 'italic',
169 em: 'italic',
170 del: 'deleted',
171 strike: 'deleted',
172 ul: 'unorderedlist',
173 ol: 'orderedlist',
174 u: 'underline',
175 tr: 'table',
176 td: 'table',
177 table: 'table'
178 },
179
180 formattingTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
181
182 linebreaks: false,
183 paragraphy: true,
184 convertDivs: true,
185 convertLinks: true,
186 convertImageLinks: false,
187 convertVideoLinks: false,
188 formattingPre: false,
189 phpTags: false,
190
191 allowedTags: false,
192 deniedTags: ['html', 'head', 'link', 'body', 'meta', 'script', 'style', 'applet'],
193
194 boldTag: 'strong',
195 italicTag: 'em',
196
197 // private
198 indentValue: 20,
199 buffer: [],
200 rebuffer: [],
201 textareamode: false,
202 emptyHtml: '<p>&#x200b;</p>',
203 invisibleSpace: '&#x200b;',
204 rBlockTest: /^(P|H[1-6]|LI|ADDRESS|SECTION|HEADER|FOOTER|ASIDE|ARTICLE)$/i,
205 alignmentTags: ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DD', 'DL', 'DT', 'DIV', 'TD',
206 'BLOCKQUOTE', 'OUTPUT', 'FIGCAPTION', 'ADDRESS', 'SECTION',
207 'HEADER', 'FOOTER', 'ASIDE', 'ARTICLE'],
208 ownLine: ['area', 'body', 'head', 'hr', 'i?frame', 'link', 'meta', 'noscript', 'style', 'script', 'table', 'tbody', 'thead', 'tfoot'],
209 contOwnLine: ['li', 'dt', 'dt', 'h[1-6]', 'option', 'script'],
210 newLevel: ['blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'p', 'pre', 'select', 'td', 'th', 'tr', 'ul'],
211 blockLevelElements: ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DD', 'DL', 'DT', 'DIV', 'LI',
212 'BLOCKQUOTE', 'OUTPUT', 'FIGCAPTION', 'PRE', 'ADDRESS', 'SECTION',
213 'HEADER', 'FOOTER', 'ASIDE', 'ARTICLE', 'TD'],
214
215 // lang
216 langs: {
217 en: {
218 html: 'HTML',
219 video: 'Insert Video',
220 image: 'Insert Image',
221 table: 'Table',
222 link: 'Link',
223 link_insert: 'Insert link',
224 link_edit: 'Edit link',
225 unlink: 'Unlink',
226 formatting: 'Formatting',
227 paragraph: 'Normal text',
228 quote: 'Quote',
229 code: 'Code',
230 header1: 'Header 1',
231 header2: 'Header 2',
232 header3: 'Header 3',
233 header4: 'Header 4',
234 header5: 'Header 5',
235 bold: 'Bold',
236 italic: 'Italic',
237 fontcolor: 'Font Color',
238 backcolor: 'Back Color',
239 unorderedlist: 'Unordered List',
240 orderedlist: 'Ordered List',
241 outdent: 'Outdent',
242 indent: 'Indent',
243 cancel: 'Cancel',
244 insert: 'Insert',
245 save: 'Save',
246 _delete: 'Delete',
247 insert_table: 'Insert Table',
248 insert_row_above: 'Add Row Above',
249 insert_row_below: 'Add Row Below',
250 insert_column_left: 'Add Column Left',
251 insert_column_right: 'Add Column Right',
252 delete_column: 'Delete Column',
253 delete_row: 'Delete Row',
254 delete_table: 'Delete Table',
255 rows: 'Rows',
256 columns: 'Columns',
257 add_head: 'Add Head',
258 delete_head: 'Delete Head',
259 title: 'Title',
260 image_position: 'Position',
261 none: 'None',
262 left: 'Left',
263 right: 'Right',
264 center: 'Center',
265 image_web_link: 'Image Web Link',
266 text: 'Text',
267 mailto: 'Email',
268 web: 'URL',
269 video_html_code: 'Video Embed Code',
270 file: 'Insert File',
271 upload: 'Upload',
272 download: 'Download',
273 choose: 'Choose',
274 or_choose: 'Or choose',
275 drop_file_here: 'Drop file here',
276 align_left: 'Align text to the left',
277 align_center: 'Center text',
278 align_right: 'Align text to the right',
279 align_justify: 'Justify text',
280 horizontalrule: 'Insert Horizontal Rule',
281 deleted: 'Deleted',
282 anchor: 'Anchor',
283 link_new_tab: 'Open link in new tab',
284 underline: 'Underline',
285 alignment: 'Alignment',
286 filename: 'Name (optional)',
287 edit: 'Edit'
288 }
289 }
290 };
291
292 // Functionality
293 Redactor.fn = $.Redactor.prototype = {
294
295 keyCode: {
296 BACKSPACE: 8,
297 DELETE: 46,
298 DOWN: 40,
299 ENTER: 13,
300 ESC: 27,
301 TAB: 9,
302 CTRL: 17,
303 META: 91,
304 LEFT: 37,
305 LEFT_WIN: 91
306 },
307
308 // Initialization
309 init: function(el, options)
310 {
311 this.rtePaste = false;
312 this.$element = this.$source = $(el);
313 this.uuid = uuid++;
314
315 // clonning options
316 var opts = $.extend(true, {}, $.Redactor.opts);
317
318 // current settings
319 this.opts = $.extend(
320 {},
321 opts,
322 this.$element.data(),
323 options
324 );
325
326 this.start = true;
327 this.dropdowns = [];
328
329 // get sizes
330 this.sourceHeight = this.$source.css('height');
331 this.sourceWidth = this.$source.css('width');
332
333 // dependency of the editor modes
334 if (this.opts.fullpage) this.opts.iframe = true;
335 if (this.opts.linebreaks) this.opts.paragraphy = false;
336 if (this.opts.paragraphy) this.opts.linebreaks = false;
337 if (this.opts.toolbarFixedBox) this.opts.toolbarFixed = true;
338
339 // the alias for iframe mode
340 this.document = document;
341 this.window = window;
342
343 // selection saved
344 this.savedSel = false;
345
346 // clean setup
347 this.cleanlineBefore = new RegExp('^<(/?' + this.opts.ownLine.join('|/?' ) + '|' + this.opts.contOwnLine.join('|') + ')[ >]');
348 this.cleanlineAfter = new RegExp('^<(br|/?' + this.opts.ownLine.join('|/?' ) + '|/' + this.opts.contOwnLine.join('|/') + ')[ >]');
349 this.cleannewLevel = new RegExp('^</?(' + this.opts.newLevel.join('|' ) + ')[ >]');
350
351 // block level
352 this.rTestBlock = new RegExp('^(' + this.opts.blockLevelElements.join('|' ) + ')$', 'i');
353
354 // setup formatting permissions
355 if (this.opts.linebreaks === false)
356 {
357 if (this.opts.allowedTags !== false)
358 {
359 var arrSearch = ['strong', 'em', 'del'];
360 var arrAdd = ['b', 'i', 'strike'];
361
362 if ($.inArray('p', this.opts.allowedTags) === '-1') this.opts.allowedTags.push('p');
363
364 for (i in arrSearch)
365 {
366 if ($.inArray(arrSearch[i], this.opts.allowedTags) != '-1') this.opts.allowedTags.push(arrAdd[i]);
367 }
368 }
369
370 if (this.opts.deniedTags !== false)
371 {
372 var pos = $.inArray('p', this.opts.deniedTags);
373 if (pos !== '-1') this.opts.deniedTags.splice(pos, pos);
374 }
375 }
376
377 // ie & opera
378 if (this.browser('msie') || this.browser('opera'))
379 {
380 this.opts.buttons = this.removeFromArrayByValue(this.opts.buttons, 'horizontalrule');
381 }
382
383 // load lang
384 this.opts.curLang = this.opts.langs[this.opts.lang];
385
386 // Build
387 this.buildStart();
388
389 },
390 toolbarInit: function(lang)
391 {
392 return {
393 html:
394 {
395 title: lang.html,
396 func: 'toggle'
397 },
398 formatting:
399 {
400 title: lang.formatting,
401 func: 'show',
402 dropdown:
403 {
404 p:
405 {
406 title: lang.paragraph,
407 func: 'formatBlocks'
408 },
409 blockquote:
410 {
411 title: lang.quote,
412 func: 'formatQuote',
413 className: 'redactor_format_blockquote'
414 },
415 pre:
416 {
417 title: lang.code,
418 func: 'formatBlocks',
419 className: 'redactor_format_pre'
420 },
421 h1:
422 {
423 title: lang.header1,
424 func: 'formatBlocks',
425 className: 'redactor_format_h1'
426 },
427 h2:
428 {
429 title: lang.header2,
430 func: 'formatBlocks',
431 className: 'redactor_format_h2'
432 },
433 h3:
434 {
435 title: lang.header3,
436 func: 'formatBlocks',
437 className: 'redactor_format_h3'
438 },
439 h4:
440 {
441 title: lang.header4,
442 func: 'formatBlocks',
443 className: 'redactor_format_h4'
444 },
445 h5:
446 {
447 title: lang.header5,
448 func: 'formatBlocks',
449 className: 'redactor_format_h5'
450 }
451 }
452 },
453 bold:
454 {
455 title: lang.bold,
456 exec: 'bold'
457 },
458 italic:
459 {
460 title: lang.italic,
461 exec: 'italic'
462 },
463 deleted:
464 {
465 title: lang.deleted,
466 exec: 'strikethrough'
467 },
468 underline:
469 {
470 title: lang.underline,
471 exec: 'underline'
472 },
473 unorderedlist:
474 {
475 title: '&bull; ' + lang.unorderedlist,
476 exec: 'insertunorderedlist'
477 },
478 orderedlist:
479 {
480 title: '1. ' + lang.orderedlist,
481 exec: 'insertorderedlist'
482 },
483 outdent:
484 {
485 title: '< ' + lang.outdent,
486 func: 'indentingOutdent'
487 },
488 indent:
489 {
490 title: '> ' + lang.indent,
491 func: 'indentingIndent'
492 },
493 image:
494 {
495 title: lang.image,
496 func: 'imageShow'
497 },
498 video:
499 {
500 title: lang.video,
501 func: 'videoShow'
502 },
503 file:
504 {
505 title: lang.file,
506 func: 'fileShow'
507 },
508 table:
509 {
510 title: lang.table,
511 func: 'show',
512 dropdown:
513 {
514 insert_table:
515 {
516 title: lang.insert_table,
517 func: 'tableShow'
518 },
519 separator_drop1:
520 {
521 name: 'separator'
522 },
523 insert_row_above:
524 {
525 title: lang.insert_row_above,
526 func: 'tableAddRowAbove'
527 },
528 insert_row_below:
529 {
530 title: lang.insert_row_below,
531 func: 'tableAddRowBelow'
532 },
533 insert_column_left:
534 {
535 title: lang.insert_column_left,
536 func: 'tableAddColumnLeft'
537 },
538 insert_column_right:
539 {
540 title: lang.insert_column_right,
541 func: 'tableAddColumnRight'
542 },
543 separator_drop2:
544 {
545 name: 'separator'
546 },
547 add_head:
548 {
549 title: lang.add_head,
550 func: 'tableAddHead'
551 },
552 delete_head:
553 {
554 title: lang.delete_head,
555 func: 'tableDeleteHead'
556 },
557 separator_drop3:
558 {
559 name: 'separator'
560 },
561 delete_column:
562 {
563 title: lang.delete_column,
564 func: 'tableDeleteColumn'
565 },
566 delete_row:
567 {
568 title: lang.delete_row,
569 func: 'tableDeleteRow'
570 },
571 delete_table:
572 {
573 title: lang.delete_table,
574 func: 'tableDeleteTable'
575 }
576 }
577 },
578 link: {
579 title: lang.link,
580 func: 'show',
581 dropdown:
582 {
583 link:
584 {
585 title: lang.link_insert,
586 func: 'linkShow'
587 },
588 unlink:
589 {
590 title: lang.unlink,
591 exec: 'unlink'
592 }
593 }
594 },
595 alignment:
596 {
597 title: lang.alignment,
598 func: 'show',
599 dropdown:
600 {
601 alignleft:
602 {
603 title: lang.align_left,
604 func: 'alignmentLeft'
605 },
606 aligncenter:
607 {
608 title: lang.align_center,
609 func: 'alignmentCenter'
610 },
611 alignright:
612 {
613 title: lang.align_right,
614 func: 'alignmentRight'
615 },
616 justify:
617 {
618 title: lang.align_justify,
619 func: 'alignmentJustify'
620 }
621 }
622 },
623 alignleft:
624 {
625 title: lang.align_left,
626 func: 'alignmentLeft'
627 },
628 aligncenter:
629 {
630 title: lang.align_center,
631 func: 'alignmentCenter'
632 },
633 alignright:
634 {
635 title: lang.align_right,
636 func: 'alignmentRight'
637 },
638 alignjustify:
639 {
640 title: lang.align_justify,
641 func: 'alignmentJustify'
642 },
643 horizontalrule:
644 {
645 exec: 'inserthorizontalrule',
646 title: lang.horizontalrule
647 }
648
649 }
650 },
651
652 // CALLBACKS
653 callback: function(type, event, data)
654 {
655 var callback = this.opts[ type + 'Callback' ];
656 if ($.isFunction(callback))
657 {
658 if (event === false) return callback.call(this, data);
659 else return callback.call(this, event, data);
660 }
661 else return data;
662 },
663
664
665 // DESTROY
666 destroy: function()
667 {
668 clearInterval(this.autosaveInterval);
669
670 $(window).off('.redactor');
671 this.$source.off('redactor-textarea');
672 this.$element.off('.redactor').removeData('redactor');
673
674 var html = this.get();
675
676 if (this.opts.textareamode)
677 {
678 this.$box.after(this.$source);
679 this.$box.remove();
680 this.$source.val(html).show();
681 }
682 else
683 {
684 var $elem = this.$editor;
685 if (this.opts.iframe) $elem = this.$element;
686
687 this.$box.after($elem);
688 this.$box.remove();
689
690 $elem.removeClass('redactor_editor').removeClass('redactor_editor_wym').removeAttr('contenteditable').html(html).show();
691 }
692
693 if (this.opts.toolbarExternal)
694 {
695 $(this.opts.toolbarExternal).html('');
696 }
697
698 if (this.opts.air)
699 {
700 $('#redactor_air_' + this.uuid).remove();
701 }
702 },
703
704 // API GET
705 getObject: function()
706 {
707 return $.extend({}, this);
708 },
709 getEditor: function()
710 {
711 return this.$editor;
712 },
713 getBox: function()
714 {
715 return this.$box;
716 },
717 getIframe: function()
718 {
719 return (this.opts.iframe) ? this.$frame : false;
720 },
721 getToolbar: function()
722 {
723 return (this.$toolbar) ? this.$toolbar : false;
724 },
725
726 // CODE GET & SET
727 get: function()
728 {
729 return this.$source.val();
730 },
731 getCodeIframe: function()
732 {
733 this.$editor.removeAttr('contenteditable').removeAttr('dir');
734 var html = this.outerHtml(this.$frame.contents().children());
735 this.$editor.attr({ 'contenteditable': true, 'dir': this.opts.direction });
736
737 return html;
738 },
739 set: function(html, strip, placeholderRemove)
740 {
741 html = html.toString();
742 html = html.replace(/\$/g, '&#36;');
743
744 if (this.opts.fullpage) this.setCodeIframe(html);
745 else this.setEditor(html, strip);
746
747 if (html == '') placeholderRemove = false;
748 if (placeholderRemove !== false) this.placeholderRemoveFromEditor();
749 },
750 setEditor: function(html, strip)
751 {
752
753 if (strip !== false)
754 {
755 html = this.cleanSavePreCode(html);
756
757 html = this.cleanStripTags(html);
758 html = this.cleanConvertProtected(html);
759 html = this.cleanConvertInlineTags(html, true);
760
761 if (this.opts.linebreaks === false) html = this.cleanConverters(html);
762 else html = html.replace(/<p(.*?)>([\w\W]*?)<\/p>/gi, '$2<br>');
763 }
764
765 // $ fix
766 html = html.replace(/&amp;#36;/g, '$');
767
768 html = this.cleanEmpty(html);
769
770 this.$editor.html(html);
771
772 // set no editable
773 this.setNonEditable();
774 this.setSpansVerified();
775
776 this.sync();
777 },
778 setCodeIframe: function(html)
779 {
780 var doc = this.iframePage();
781 this.$frame[0].src = "about:blank";
782
783 html = this.cleanConvertProtected(html);
784 html = this.cleanConvertInlineTags(html);
785 html = this.cleanRemoveSpaces(html);
786
787 doc.open();
788 doc.write(html);
789 doc.close();
790
791 // redefine editor for fullpage mode
792 if (this.opts.fullpage)
793 {
794 this.$editor = this.$frame.contents().find('body').attr({ 'contenteditable': true, 'dir': this.opts.direction });
795 }
796
797 // set no editable
798 this.setNonEditable();
799 this.setSpansVerified();
800 this.sync();
801
802 },
803 setFullpageOnInit: function(html)
804 {
805 html = this.cleanSavePreCode(html, true);
806 html = this.cleanConverters(html);
807 html = this.cleanEmpty(html);
808
809 // set code
810 this.$editor.html(html);
811
812 // set no editable
813 this.setNonEditable();
814 this.setSpansVerified();
815 this.sync();
816 },
817 setSpansVerified: function()
818 {
819 var spans = this.$editor.find('span');
820 var replacementTag = 'inline';
821
822 $.each(spans, function() {
823 var outer = this.outerHTML;
824
825 // Replace opening tag
826 var regex = new RegExp('<' + this.tagName, 'gi');
827 var newTag = outer.replace(regex, '<' + replacementTag);
828
829 // Replace closing tag
830 regex = new RegExp('</' + this.tagName, 'gi');
831 newTag = newTag.replace(regex, '</' + replacementTag);
832
833 $(this).replaceWith(newTag);
834 });
835
836 },
837 setSpansVerifiedHtml: function(html)
838 {
839 html = html.replace(/<span(.*?)>/, '<inline$1>');
840 return html.replace(/<\/span>/, '</inline>');
841 },
842 setNonEditable: function()
843 {
844 this.$editor.find('.noneditable').attr('contenteditable', false);
845 },
846
847 // SYNC
848 sync: function(e)
849 {
850 var html = '';
851
852 this.cleanUnverified();
853
854 if (this.opts.fullpage) html = this.getCodeIframe();
855 else html = this.$editor.html();
856
857 html = this.syncClean(html);
858 html = this.cleanRemoveEmptyTags(html);
859
860 // is there a need to synchronize
861 var source = this.cleanRemoveSpaces(this.$source.val(), false);
862 var editor = this.cleanRemoveSpaces(html, false);
863
864 if (source == editor)
865 {
866 // do not sync
867 return false;
868 }
869
870
871 // fix second level up ul, ol
872 html = html.replace(/<\/li><(ul|ol)>([\w\W]*?)<\/(ul|ol)>/gi, '<$1>$2</$1></li>');
873
874 if ($.trim(html) === '<br>') html = '';
875
876 // xhtml
877 if (this.opts.xhtml)
878 {
879 var xhtmlTags = ['br', 'hr', 'img', 'link', 'input', 'meta'];
880 $.each(xhtmlTags, function(i,s)
881 {
882 html = html.replace(new RegExp('<' + s + '(.*?[^\/$]?)>', 'gi'), '<' + s + '$1 />');
883 });
884
885 }
886
887 // before callback
888 html = this.callback('syncBefore', false, html);
889
890 this.$source.val(html);
891
892 // onchange & after callback
893 this.callback('syncAfter', false, html);
894
895 if (this.start === false)
896 {
897
898 if (typeof e != 'undefined')
899 {
900 switch(e.which)
901 {
902 case 37: // left
903 break;
904 case 38: // up
905 break;
906 case 39: // right
907 break;
908 case 40: // down
909 break;
910
911 default: this.callback('change', false, html);
912 }
913 }
914 else
915 {
916 this.callback('change', false, html);
917 }
918 }
919
920 },
921 syncClean: function(html)
922 {
923 if (!this.opts.fullpage) html = this.cleanStripTags(html);
924
925 // trim
926 html = $.trim(html);
927
928 // removeplaceholder
929 html = this.placeholderRemoveFromCode(html);
930
931 // remove space
932 html = html.replace(/&#x200b;/gi, '');
933 html = html.replace(/&#8203;/gi, '');
934 html = html.replace(/<\/a>&nbsp;/gi, '<\/a> ');
935 html = html.replace(/\u200B/g, '');
936
937 if (html == '<p></p>' || html == '<p> </p>' || html == '<p>&nbsp;</p>')
938 {
939 html = '';
940 }
941
942 // link nofollow
943 if (this.opts.linkNofollow)
944 {
945 html = html.replace(/<a(.*?)rel="nofollow"(.*?)>/gi, '<a$1$2>');
946 html = html.replace(/<a(.*?)>/gi, '<a$1 rel="nofollow">');
947 }
948
949 // php code fix
950 html = html.replace('<!--?php', '<?php');
951 html = html.replace('?-->', '?>');
952
953 // revert no editable
954 html = html.replace(/<(.*?)class="noeditable"(.*?) contenteditable="false"(.*?)>/gi, '<$1class="noeditable"$2$3>');
955
956 html = html.replace(/ data-tagblock=""/gi, '');
957 html = html.replace(/<br\s?\/?>\n?<\/(P|H[1-6]|LI|ADDRESS|SECTION|HEADER|FOOTER|ASIDE|ARTICLE)>/gi, '</$1>');
958
959 // remove image resize
960 html = html.replace(/<span(.*?)id="redactor-image-box"(.*?)>([\w\W]*?)<img(.*?)><\/span>/gi, '$3<img$4>');
961 html = html.replace(/<span(.*?)id="redactor-image-resizer"(.*?)>(.*?)<\/span>/gi, '');
962 html = html.replace(/<span(.*?)id="redactor-image-editter"(.*?)>(.*?)<\/span>/gi, '');
963
964 // remove empty lists
965 html = html.replace(/<(ul|ol)>\s*\t*\n*<\/(ul|ol)>/gi, '');
966
967 // remove font
968 if (this.opts.cleanFontTag)
969 {
970 html = html.replace(/<font(.*?)>([\w\W]*?)<\/font>/gi, '$2');
971 }
972
973 // remove spans
974 html = html.replace(/<span(.*?)>([\w\W]*?)<\/span>/gi, '$2');
975 html = html.replace(/<inline>/gi, '<span>');
976 html = html.replace(/<inline /gi, '<span ');
977 html = html.replace(/<\/inline>/gi, '</span>');
978 html = html.replace(/<span(.*?)class="redactor_placeholder"(.*?)>([\w\W]*?)<\/span>/gi, '');
979 html = html.replace(/<span>([\w\W]*?)<\/span>/gi, '$1');
980
981 // special characters
982 html = html.replace(/&amp;/gi, '&');
983 html = html.replace(/™/gi, '&trade;');
984 html = html.replace(/©/gi, '&copy;');
985 html = html.replace(/…/gi, '&hellip;');
986 html = html.replace(/—/gi, '&mdash;');
987 html = html.replace(/‐/gi, '&dash;');
988
989
990 html = this.cleanReConvertProtected(html);
991
992 return html;
993 },
994
995
996 // BUILD
997 buildStart: function()
998 {
999 // content
1000 this.content = '';
1001
1002 // container
1003 this.$box = $('<div class="redactor_box" />');
1004 this.$box.css('z-index', 100-this.uuid);
1005
1006 // textarea test
1007 if (this.$source[0].tagName === 'TEXTAREA') this.opts.textareamode = true;
1008
1009 // mobile
1010 if (this.opts.mobile === false && this.isMobile())
1011 {
1012 this.buildMobile();
1013 }
1014 else
1015 {
1016 // get the content at the start
1017 this.buildContent();
1018
1019 if (this.opts.iframe)
1020 {
1021 // build as iframe
1022 this.opts.autoresize = false;
1023 this.iframeStart();
1024 }
1025 else if (this.opts.textareamode) this.buildFromTextarea();
1026 else this.buildFromElement();
1027
1028 // options and final setup
1029 if (!this.opts.iframe)
1030 {
1031 this.buildOptions();
1032 this.buildAfter();
1033 }
1034 }
1035 },
1036 buildMobile: function()
1037 {
1038 if (!this.opts.textareamode)
1039 {
1040 this.$editor = this.$source;
1041 this.$editor.hide();
1042 this.$source = this.buildCodearea(this.$editor);
1043 this.$source.val(this.content);
1044 }
1045
1046 this.$box.insertAfter(this.$source).append(this.$source);
1047 },
1048 buildContent: function()
1049 {
1050 if (this.opts.textareamode) this.content = $.trim(this.$source.val());
1051 else this.content = $.trim(this.$source.html());
1052 },
1053 buildFromTextarea: function()
1054 {
1055 this.$editor = $('<div />');
1056 this.$box.insertAfter(this.$source).append(this.$editor).append(this.$source);
1057
1058 // enable
1059 this.buildAddClasses(this.$editor);
1060 this.buildEnable();
1061 },
1062 buildFromElement: function()
1063 {
1064 this.$editor = this.$source;
1065 this.$source = this.buildCodearea(this.$editor);
1066 this.$box.insertAfter(this.$editor).append(this.$editor).append(this.$source);
1067
1068 // enable
1069 this.buildEnable();
1070 },
1071 buildCodearea: function($source)
1072 {
1073 return $('<textarea />').attr('name', $source.attr('id')).css('height', this.sourceHeight);
1074 },
1075 buildAddClasses: function(el)
1076 {
1077 // append textarea classes to editable layer
1078 $.each(this.$source.get(0).className.split(/\s+/), function(i,s)
1079 {
1080 el.addClass('redactor_' + s);
1081 });
1082 },
1083 buildEnable: function()
1084 {
1085 this.$editor.addClass('redactor_editor').attr({ 'contenteditable': true, 'dir': this.opts.direction });
1086 this.$source.attr('dir', this.opts.direction).hide();
1087
1088 // set code
1089 this.set(this.content, true, false);
1090 },
1091 buildOptions: function()
1092 {
1093 var $source = this.$editor;
1094 if (this.opts.iframe) $source = this.$frame;
1095
1096 // options
1097 if (this.opts.tabindex) $source.attr('tabindex', this.opts.tabindex);
1098
1099 if (this.opts.minHeight) $source.css('min-height', this.opts.minHeight + 'px');
1100 // FF fix bug with line-height rendering
1101 else if (this.browser('mozilla') && this.opts.linebreaks)
1102 {
1103 this.$editor.css('min-height', '45px');
1104 }
1105 // FF fix bug with line-height rendering
1106 if (this.browser('mozilla') && this.opts.linebreaks)
1107 {
1108 this.$editor.css('padding-bottom', '10px');
1109 }
1110
1111
1112 if (this.opts.maxHeight)
1113 {
1114 this.opts.autoresize = false;
1115 this.sourceHeight = this.opts.maxHeight;
1116 }
1117 if (this.opts.wym) this.$editor.addClass('redactor_editor_wym');
1118 if (this.opts.typewriter) this.$editor.addClass('redactor-editor-typewriter');
1119 if (!this.opts.autoresize) $source.css('height', this.sourceHeight);
1120
1121 },
1122 buildAfter: function()
1123 {
1124 this.start = false;
1125
1126 // load toolbar
1127 if (this.opts.toolbar)
1128 {
1129 this.opts.toolbar = this.toolbarInit(this.opts.curLang);
1130 this.toolbarBuild();
1131 }
1132
1133 // modal templates
1134 this.modalTemplatesInit();
1135
1136 // plugins
1137 this.buildPlugins();
1138
1139 // enter, tab, etc.
1140 this.buildBindKeyboard();
1141
1142 // autosave
1143 if (this.opts.autosave) this.autosave();
1144
1145 // observers
1146 setTimeout($.proxy(this.observeStart, this), 4);
1147
1148 // FF fix
1149 if (this.browser('mozilla'))
1150 {
1151 try {
1152 this.document.execCommand('enableObjectResizing', false, false);
1153 this.document.execCommand('enableInlineTableEditing', false, false);
1154 } catch (e) {}
1155 }
1156
1157 // focus
1158 if (this.opts.focus) setTimeout($.proxy(this.focus, this), 100);
1159
1160 // code mode
1161 if (!this.opts.visual)
1162 {
1163 setTimeout($.proxy(function()
1164 {
1165 this.opts.visual = true;
1166 this.toggle(false);
1167
1168 }, this), 200);
1169 }
1170
1171 // init callback
1172 this.callback('init');
1173 },
1174 buildBindKeyboard: function()
1175 {
1176 this.dblEnter = 0;
1177
1178 if (this.opts.dragUpload && this.opts.imageUpload !== false)
1179 {
1180 this.$editor.on('drop.redactor', $.proxy(this.buildEventDrop, this));
1181 }
1182
1183 this.$editor.on('input.redactor', $.proxy(this.sync, this));
1184 this.$editor.on('paste.redactor', $.proxy(this.buildEventPaste, this));
1185 this.$editor.on('keydown.redactor', $.proxy(this.buildEventKeydown, this));
1186 this.$editor.on('keyup.redactor', $.proxy(this.buildEventKeyup, this));
1187
1188 // textarea callback
1189 if ($.isFunction(this.opts.textareaKeydownCallback))
1190 {
1191 this.$source.on('keydown.redactor-textarea', $.proxy(this.opts.textareaKeydownCallback, this));
1192 }
1193
1194 // focus callback
1195 if ($.isFunction(this.opts.focusCallback))
1196 {
1197 this.$editor.on('focus.redactor', $.proxy(this.opts.focusCallback, this));
1198 }
1199
1200 var clickedElement;
1201 $(document).mousedown(function(e) {
1202 clickedElement = $(e.target);
1203 });
1204
1205 // blur callback
1206 this.$editor.on('blur.redactor', $.proxy(function(e)
1207 {
1208 if (!$(clickedElement).hasClass('redactor_toolbar') && $(clickedElement).parents('.redactor_toolbar').size() == 0)
1209 {
1210 this.selectall = false;
1211 if ($.isFunction(this.opts.blurCallback)) this.callback('blur', e);
1212 }
1213 }, this));
1214
1215 },
1216 buildEventDrop: function(e)
1217 {
1218 e = e.originalEvent || e;
1219
1220 if (window.FormData === undefined || !e.dataTransfer) return true;
1221
1222 var length = e.dataTransfer.files.length;
1223 if (length == 0) return true;
1224
1225 e.preventDefault();
1226
1227 var file = e.dataTransfer.files[0];
1228
1229 if (this.opts.dnbImageTypes !== false && this.opts.dnbImageTypes.indexOf(file.type) == -1)
1230 {
1231 return true;
1232 }
1233
1234 this.bufferSet();
1235
1236 var progress = $('<div id="redactor-progress"><span></span></div>');
1237 $(document.body).append(progress);
1238
1239 if (this.opts.s3 === false)
1240 {
1241 this.dragUploadAjax(this.opts.imageUpload, file, true, progress, e, this.opts.imageUploadParam);
1242 }
1243 else
1244 {
1245 this.s3uploadFile(file);
1246 }
1247
1248
1249 },
1250 buildEventPaste: function(e)
1251 {
1252 var oldsafari = false;
1253 if (this.browser('webkit') && navigator.userAgent.indexOf('Chrome') === -1)
1254 {
1255 var arr = this.browser('version').split('.');
1256 if (arr[0] < 536) oldsafari = true;
1257 }
1258
1259 if (oldsafari) return true;
1260
1261 // paste except opera (not webkit)
1262 if (this.browser('opera')) return true;
1263
1264 // clipboard upload
1265 if (this.opts.clipboardUpload && this.buildEventClipboardUpload(e)) return true;
1266
1267 if (this.opts.cleanup)
1268 {
1269 this.rtePaste = true;
1270
1271 this.selectionSave();
1272
1273 if (!this.selectall)
1274 {
1275 if (this.opts.autoresize === true && this.fullscreen !== true)
1276 {
1277 this.$editor.height(this.$editor.height());
1278 this.saveScroll = this.document.body.scrollTop;
1279 }
1280 else
1281 {
1282 this.saveScroll = this.$editor.scrollTop();
1283 }
1284 }
1285
1286 var frag = this.extractContent();
1287
1288 setTimeout($.proxy(function()
1289 {
1290 var pastedFrag = this.extractContent();
1291 this.$editor.append(frag);
1292
1293 this.selectionRestore();
1294
1295 var html = this.getFragmentHtml(pastedFrag);
1296 this.pasteClean(html);
1297
1298 if (this.opts.autoresize === true && this.fullscreen !== true) this.$editor.css('height', 'auto');
1299
1300 }, this), 1);
1301 }
1302 },
1303 buildEventClipboardUpload: function(e)
1304 {
1305 var event = e.originalEvent || e;
1306 this.clipboardFilePaste = false;
1307
1308 if (typeof(event.clipboardData) === 'undefined') return false;
1309 if (event.clipboardData.items)
1310 {
1311 var file = event.clipboardData.items[0].getAsFile();
1312 if (file !== null)
1313 {
1314 this.bufferSet();
1315 this.clipboardFilePaste = true;
1316
1317 var reader = new FileReader();
1318 reader.onload = $.proxy(this.pasteClipboardUpload, this);
1319 reader.readAsDataURL(file);
1320
1321 return true;
1322 }
1323 }
1324
1325 return false;
1326
1327 },
1328 buildEventKeydown: function(e)
1329 {
1330 if (this.rtePaste) return false;
1331
1332 var key = e.which;
1333 var ctrl = e.ctrlKey || e.metaKey;
1334 var parent = this.getParent();
1335 var current = this.getCurrent();
1336 var block = this.getBlock();
1337 var pre = false;
1338
1339 this.callback('keydown', e);
1340
1341 // disabling cmd|ctrl + left
1342 if (this.browser('mozilla') && ctrl && key === 37)
1343 {
1344 e.preventDefault();
1345 return false;
1346 }
1347
1348 this.imageResizeHide(false);
1349
1350 // pre & down
1351 if ((parent && $(parent).get(0).tagName === 'PRE') || (current && $(current).get(0).tagName === 'PRE'))
1352 {
1353 pre = true;
1354 if (key === this.keyCode.DOWN) this.insertAfterLastElement(block);
1355 }
1356
1357 // down
1358 if (key === this.keyCode.DOWN)
1359 {
1360 if (parent && $(parent)[0].tagName === 'BLOCKQUOTE') this.insertAfterLastElement(parent);
1361 if (current && $(current)[0].tagName === 'BLOCKQUOTE') this.insertAfterLastElement(current);
1362
1363 if (parent && $(parent)[0].tagName === 'P' && $(parent).parent()[0].tagName == 'BLOCKQUOTE')
1364 {
1365 this.insertAfterLastElement(parent, $(parent).parent()[0]);
1366 }
1367 if (current && $(current)[0].tagName === 'P' && parent && $(parent)[0].tagName == 'BLOCKQUOTE')
1368 {
1369 this.insertAfterLastElement(current, parent);
1370 }
1371 }
1372
1373 // shortcuts setup
1374 if (ctrl && !e.shiftKey) this.shortcuts(e, key);
1375
1376 // buffer setup
1377 if (ctrl && key === 90 && !e.shiftKey && !e.altKey) // z key
1378 {
1379 e.preventDefault();
1380 if (this.opts.buffer.length) this.bufferUndo();
1381 else this.document.execCommand('undo', false, false);
1382 return;
1383 }
1384 // undo
1385 else if (ctrl && key === 90 && e.shiftKey && !e.altKey)
1386 {
1387 e.preventDefault();
1388 if (this.opts.rebuffer.length != 0) this.bufferRedo();
1389 else this.document.execCommand('redo', false, false);
1390 return;
1391 }
1392
1393 // space
1394 if (key == 32)
1395 {
1396 this.bufferSet();
1397 }
1398
1399 // select all
1400 if (ctrl && key === 65)
1401 {
1402 this.bufferSet();
1403 this.selectall = true;
1404 }
1405 else if (key != this.keyCode.LEFT_WIN && !ctrl)
1406 {
1407 this.selectall = false;
1408 }
1409
1410 // enter
1411 if (key == this.keyCode.ENTER && !e.shiftKey && !e.ctrlKey && !e.metaKey )
1412 {
1413 //
1414 var range = this.getRange();
1415 if (range && range.collapsed === false)
1416 {
1417 sel = this.getSelection();
1418 if (sel.rangeCount)
1419 {
1420 range.deleteContents();
1421 }
1422 }
1423
1424 // In ie, opera in the tables are created paragraphs, fix it.
1425 if (this.browser('msie') && (parent.nodeType == 1 && (parent.tagName == 'TD' || parent.tagName == 'TH')))
1426 {
1427 e.preventDefault();
1428 this.bufferSet();
1429 this.insertNode(document.createElement('br'));
1430 this.callback('enter', e);
1431 return false;
1432 }
1433
1434 // blockquote exit
1435 if (block && (block.tagName == 'BLOCKQUOTE' || $(block).parent()[0].tagName == 'BLOCKQUOTE'))
1436 {
1437 if (this.isEndOfElement())
1438 {
1439 if (this.dblEnter == 1)
1440 {
1441 var element;
1442 var last;
1443 if (block.tagName == 'BLOCKQUOTE')
1444 {
1445 last = 'br';
1446 element = block;
1447 }
1448 else
1449 {
1450 last = 'p';
1451 element = $(block).parent()[0];
1452 }
1453
1454 e.preventDefault();
1455 this.insertingAfterLastElement(element);
1456 this.dblEnter = 0;
1457
1458 if (last == 'p')
1459 {
1460 $(block).parent().find('p').last().remove();
1461 }
1462 else
1463 {
1464 var tmp = $.trim($(block).html());
1465 $(block).html(tmp.replace(/<br\s?\/?>$/i, ''));
1466 }
1467
1468 return;
1469 }
1470 else this.dblEnter++;
1471 }
1472 else this.dblEnter++;
1473 }
1474
1475 // pre
1476 if (pre === true) return this.buildEventKeydownPre(e, current);
1477 else
1478 {
1479 if (!this.opts.linebreaks)
1480 {
1481 // replace div to p
1482 if (block && this.opts.rBlockTest.test(block.tagName))
1483 {
1484 // hit enter
1485 this.bufferSet();
1486
1487 setTimeout($.proxy(function()
1488 {
1489 var blockElem = this.getBlock();
1490 if (blockElem.tagName === 'DIV' && !$(blockElem).hasClass('redactor_editor'))
1491 {
1492 var node = $('<p>' + this.opts.invisibleSpace + '</p>');
1493 $(blockElem).replaceWith(node);
1494 this.selectionStart(node);
1495 }
1496
1497 }, this), 1);
1498 }
1499 else if (block === false)
1500 {
1501 // hit enter
1502 this.bufferSet();
1503
1504 var node = $('<p>' + this.opts.invisibleSpace + '</p>');
1505 this.insertNode(node[0]);
1506 this.selectionStart(node);
1507 this.callback('enter', e);
1508 return false;
1509 }
1510
1511 }
1512
1513 if (this.opts.linebreaks)
1514 {
1515 // replace div to br
1516 if (block && this.opts.rBlockTest.test(block.tagName))
1517 {
1518 // hit enter
1519 this.bufferSet();
1520
1521 setTimeout($.proxy(function()
1522 {
1523 var blockElem = this.getBlock();
1524 if ((blockElem.tagName === 'DIV' || blockElem.tagName === 'P') && !$(blockElem).hasClass('redactor_editor'))
1525 {
1526 this.replaceLineBreak(blockElem);
1527 }
1528
1529 }, this), 1);
1530 }
1531 else
1532 {
1533 return this.buildEventKeydownInsertLineBreak(e);
1534 }
1535 }
1536
1537 // blockquote, figcaption
1538 if (block.tagName == 'BLOCKQUOTE' || block.tagName == 'FIGCAPTION')
1539 {
1540 return this.buildEventKeydownInsertLineBreak(e);
1541 }
1542
1543 }
1544
1545 this.callback('enter', e);
1546 }
1547 else if (key === this.keyCode.ENTER && (e.ctrlKey || e.shiftKey)) // Shift+Enter or Ctrl+Enter
1548 {
1549 this.bufferSet();
1550
1551 e.preventDefault();
1552 this.insertLineBreak();
1553 }
1554
1555 // tab (cmd + [)
1556 if ((key === this.keyCode.TAB || e.metaKey && key === 219) && this.opts.shortcuts)
1557 {
1558 return this.buildEventKeydownTab(e, pre, key);
1559 }
1560
1561 // delete zero-width space before the removing
1562 if (key === this.keyCode.BACKSPACE) this.buildEventKeydownBackspace(current);
1563
1564 },
1565 buildEventKeydownPre: function(e, current)
1566 {
1567 e.preventDefault();
1568 this.bufferSet();
1569 var html = $(current).parent().text();
1570 this.insertNode(document.createTextNode('\n'));
1571 if (html.search(/\s$/) == -1)
1572 {
1573 this.insertNode(document.createTextNode('\n'));
1574 }
1575
1576 this.sync();
1577 this.callback('enter', e);
1578 return false;
1579 },
1580 buildEventKeydownTab: function(e, pre, key)
1581 {
1582 if (!this.opts.tabFocus) return true;
1583 if (this.isEmpty(this.get()) && this.opts.tabSpaces === false) return true;
1584
1585 e.preventDefault();
1586
1587 if (pre === true && !e.shiftKey)
1588 {
1589 this.bufferSet();
1590 this.insertNode(document.createTextNode('\t'));
1591 this.sync();
1592 return false;
1593
1594 }
1595 else if (this.opts.tabSpaces !== false)
1596 {
1597 this.bufferSet();
1598 this.insertNode(document.createTextNode(Array(this.opts.tabSpaces + 1).join('\u00a0')));
1599 this.sync();
1600 return false;
1601 }
1602 else
1603 {
1604 if (!e.shiftKey) this.indentingIndent();
1605 else this.indentingOutdent();
1606 }
1607
1608 return false;
1609 },
1610 buildEventKeydownBackspace: function(current)
1611 {
1612 if (typeof current.tagName !== 'undefined' && /^(H[1-6])$/i.test(current.tagName))
1613 {
1614 var node;
1615 if (this.opts.linebreaks === false) node = $('<p>' + this.opts.invisibleSpace + '</p>');
1616 else node = $('<br>' + this.opts.invisibleSpace);
1617
1618 $(current).replaceWith(node);
1619 this.selectionStart(node);
1620 }
1621
1622 if (typeof current.nodeValue !== 'undefined' && current.nodeValue !== null)
1623 {
1624
1625 //var value = $.trim(current.nodeValue.replace(/[^\u0000-\u1C7F]/g, ''));
1626 if (current.remove && current.nodeType === 3 && current.nodeValue.match(/[^\u200B]/g) == null)
1627 {
1628 current.remove();
1629 }
1630 }
1631 },
1632 buildEventKeydownInsertLineBreak: function(e)
1633 {
1634 this.bufferSet();
1635 e.preventDefault();
1636 this.insertLineBreak();
1637 this.callback('enter', e);
1638 return;
1639 },
1640 buildEventKeyup: function(e)
1641 {
1642 if (this.rtePaste) return false;
1643
1644 var key = e.which;
1645 var parent = this.getParent();
1646 var current = this.getCurrent();
1647
1648 // replace to p before / after the table or body
1649 if (!this.opts.linebreaks && current.nodeType == 3 && (parent == false || parent.tagName == 'BODY'))
1650 {
1651 var node = $('<p>').append($(current).clone());
1652 $(current).replaceWith(node);
1653 var next = $(node).next();
1654 if (typeof(next[0]) !== 'undefined' && next[0].tagName == 'BR')
1655 {
1656 next.remove();
1657 }
1658
1659 this.selectionEnd(node);
1660 }
1661
1662 // convert links
1663 if ((this.opts.convertLinks || this.opts.convertImageLinks || this.opts.convertVideoLinks) && key === this.keyCode.ENTER)
1664 {
1665 this.buildEventKeyupConverters();
1666 }
1667
1668 // if empty
1669 if (key === this.keyCode.DELETE || key === this.keyCode.BACKSPACE)
1670 {
1671 return this.formatEmpty(e);
1672 }
1673
1674 this.callback('keyup', e);
1675 this.sync(e);
1676 },
1677 buildEventKeyupConverters: function()
1678 {
1679 this.formatLinkify(this.opts.linkProtocol, this.opts.convertLinks, this.opts.convertImageLinks, this.opts.convertVideoLinks, this.opts.linkSize);
1680
1681 setTimeout($.proxy(function()
1682 {
1683 if (this.opts.convertImageLinks) this.observeImages();
1684 if (this.opts.observeLinks) this.observeLinks();
1685 }, this), 5);
1686 },
1687 buildPlugins: function()
1688 {
1689 if (!this.opts.plugins ) return;
1690
1691 $.each(this.opts.plugins, $.proxy(function(i, s)
1692 {
1693 if (RedactorPlugins[s])
1694 {
1695 $.extend(this, RedactorPlugins[s]);
1696 if ($.isFunction( RedactorPlugins[ s ].init)) this.init();
1697 }
1698
1699 }, this ));
1700 },
1701
1702 // IFRAME
1703 iframeStart: function()
1704 {
1705 this.iframeCreate();
1706
1707 if (this.opts.textareamode) this.iframeAppend(this.$source);
1708 else
1709 {
1710 this.$sourceOld = this.$source.hide();
1711 this.$source = this.buildCodearea(this.$sourceOld);
1712 this.iframeAppend(this.$sourceOld);
1713 }
1714 },
1715 iframeAppend: function(el)
1716 {
1717 this.$source.attr('dir', this.opts.direction).hide();
1718 this.$box.insertAfter(el).append(this.$frame).append(this.$source);
1719 },
1720 iframeCreate: function()
1721 {
1722 this.$frame = $('<iframe style="width: 100%;" frameborder="0" />').one('load', $.proxy(function()
1723 {
1724 if (this.opts.fullpage)
1725 {
1726 this.iframePage();
1727
1728 if (this.content === '') this.content = this.opts.invisibleSpace;
1729
1730 this.$frame.contents()[0].write(this.content);
1731 this.$frame.contents()[0].close();
1732
1733 var timer = setInterval($.proxy(function()
1734 {
1735 if (this.$frame.contents().find('body').html())
1736 {
1737 clearInterval(timer);
1738 this.iframeLoad();
1739 }
1740
1741 }, this), 0);
1742 }
1743 else this.iframeLoad();
1744
1745 }, this));
1746 },
1747 iframeDoc: function()
1748 {
1749 return this.$frame[0].contentWindow.document;
1750 },
1751 iframePage: function()
1752 {
1753 var doc = this.iframeDoc();
1754 if (doc.documentElement) doc.removeChild(doc.documentElement);
1755
1756 return doc;
1757 },
1758 iframeAddCss: function(css)
1759 {
1760 css = css || this.opts.css;
1761
1762 if (this.isString(css))
1763 {
1764 this.$frame.contents().find('head').append('<link rel="stylesheet" href="' + css + '" />');
1765 }
1766
1767 if ($.isArray(css))
1768 {
1769 $.each(css, $.proxy(function(i, url)
1770 {
1771 this.iframeAddCss(url);
1772
1773 }, this));
1774 }
1775 },
1776 iframeLoad: function()
1777 {
1778 this.$editor = this.$frame.contents().find('body').attr({ 'contenteditable': true, 'dir': this.opts.direction });
1779
1780 // set document & window
1781 if (this.$editor[0])
1782 {
1783 this.document = this.$editor[0].ownerDocument;
1784 this.window = this.document.defaultView || window;
1785 }
1786
1787 // iframe css
1788 this.iframeAddCss();
1789
1790 if (this.opts.fullpage) this.setFullpageOnInit(this.$editor.html());
1791 else this.set(this.content, true, false);
1792
1793 this.buildOptions();
1794 this.buildAfter();
1795 },
1796
1797 // PLACEHOLDER
1798 placeholderStart: function(html)
1799 {
1800 if (this.isEmpty(html))
1801 {
1802 if (this.$element.attr('placeholder'))
1803 {
1804 this.opts.placeholder = this.$element.attr('placeholder');
1805 }
1806
1807 if (this.opts.placeholder !== '')
1808 {
1809 this.opts.focus = false;
1810 this.placeholderOnFocus();
1811 this.placeholderOnBlur();
1812
1813 return this.placeholderGet();
1814 }
1815 }
1816 else
1817 {
1818 this.placeholderOnBlur();
1819 }
1820
1821 return false;
1822 },
1823 placeholderOnFocus: function()
1824 {
1825 this.$editor.on('focus.redactor_placeholder', $.proxy(this.placeholderFocus, this));
1826 },
1827 placeholderOnBlur: function()
1828 {
1829 this.$editor.on('blur.redactor_placeholder', $.proxy(this.placeholderBlur, this));
1830 },
1831 placeholderGet: function()
1832 {
1833 return $('<span class="redactor_placeholder">').data('redactor', 'verified').attr('contenteditable', false).text(this.opts.placeholder);
1834 },
1835 placeholderBlur: function()
1836 {
1837 var html = this.get();
1838 if (this.isEmpty(html))
1839 {
1840 this.placeholderOnFocus();
1841 this.$editor.html(this.placeholderGet());
1842 }
1843 },
1844 placeholderFocus: function()
1845 {
1846 this.$editor.find('span.redactor_placeholder').remove();
1847
1848 var html = '';
1849 if (this.opts.linebreaks === false)
1850 {
1851 html = this.opts.emptyHtml;
1852 }
1853
1854 this.$editor.off('focus.redactor_placeholder');
1855 this.$editor.html(html);
1856
1857 if (this.opts.linebreaks === false)
1858 {
1859 // place the cursor inside emptyHtml
1860 this.selectionStart(this.$editor.children()[0]);
1861 }
1862 else
1863 {
1864 this.focus();
1865 }
1866
1867 this.sync();
1868 },
1869 placeholderRemoveFromEditor: function()
1870 {
1871 this.$editor.find('span.redactor_placeholder').remove();
1872 this.$editor.off('focus.redactor_placeholder');
1873 },
1874 placeholderRemoveFromCode: function(html)
1875 {
1876 return html.replace(/<span class="redactor_placeholder"(.*?)>(.*?)<\/span>/i, '');
1877 },
1878
1879 // SHORTCUTS
1880 shortcuts: function(e, key)
1881 {
1882
1883 if (!this.opts.shortcuts)
1884 {
1885 if (key === 66 || key === 73) e.preventDefault();
1886 return false;
1887 }
1888
1889 if (key === 77) this.shortcutsLoad(e, 'removeFormat'); // Ctrl + m
1890 else if (key === 66) this.shortcutsLoad(e, 'bold'); // Ctrl + b
1891 else if (key === 73) this.shortcutsLoad(e, 'italic'); // Ctrl + i
1892
1893 else if (key === 74) this.shortcutsLoad(e, 'insertunorderedlist'); // Ctrl + j
1894 else if (key === 75) this.shortcutsLoad(e, 'insertorderedlist'); // Ctrl + k
1895
1896 else if (key === 72) this.shortcutsLoad(e, 'superscript'); // Ctrl + h
1897 else if (key === 76) this.shortcutsLoad(e, 'subscript'); // Ctrl + l
1898
1899 },
1900 shortcutsLoad: function(e, cmd)
1901 {
1902 e.preventDefault();
1903 this.execCommand(cmd, false);
1904 },
1905 shortcutsLoadFormat: function(e, cmd)
1906 {
1907 e.preventDefault();
1908 this.formatBlocks(cmd);
1909 },
1910
1911 // FOCUS
1912 focus: function()
1913 {
1914 if (!this.browser('opera'))
1915 {
1916 this.window.setTimeout($.proxy(this.focusSet, this, true), 1);
1917 }
1918 else
1919 {
1920 this.$editor.focus();
1921 }
1922 },
1923 focusWithSaveScroll: function()
1924 {
1925 if (this.browser('msie'))
1926 {
1927 var top = this.document.documentElement.scrollTop;
1928 }
1929
1930 this.$editor.focus();
1931
1932 if (this.browser('msie'))
1933 {
1934 this.document.documentElement.scrollTop = top;
1935 }
1936 },
1937 focusEnd: function()
1938 {
1939 if (!this.browser('mozilla'))
1940 {
1941 this.focusSet();
1942 }
1943 else
1944 {
1945 if (this.opts.linebreaks === false)
1946 {
1947 var last = this.$editor.children().last();
1948
1949 this.$editor.focus();
1950 this.selectionEnd(last);
1951 }
1952 else
1953 {
1954 this.focusSet();
1955 }
1956 }
1957 },
1958 focusSet: function(collapse, element)
1959 {
1960 this.$editor.focus();
1961
1962 if (typeof element == 'undefined')
1963 {
1964 element = this.$editor[0];
1965 }
1966
1967 var range = this.getRange();
1968 range.selectNodeContents(element);
1969
1970 // collapse - controls the position of focus: the beginning (true), at the end (false).
1971 range.collapse(collapse || false);
1972
1973 var sel = this.getSelection();
1974 sel.removeAllRanges();
1975 sel.addRange(range);
1976 },
1977
1978 // TOGGLE
1979 toggle: function(direct)
1980 {
1981 if (this.opts.visual) this.toggleCode(direct);
1982 else this.toggleVisual();
1983 },
1984 toggleVisual: function()
1985 {
1986 var html = this.$source.hide().val();
1987
1988 if (typeof this.modified !== 'undefined')
1989 {
1990 this.modified = this.cleanRemoveSpaces(this.modified, false) !== this.cleanRemoveSpaces(html, false);
1991 }
1992
1993 if (this.modified)
1994 {
1995 // don't remove the iframe even if cleared all.
1996 if (this.opts.fullpage && html === '') this.setFullpageOnInit(html);
1997 else
1998 {
1999 this.set(html);
2000 if (this.opts.fullpage) this.buildBindKeyboard();
2001 }
2002 }
2003
2004 if (this.opts.iframe) this.$frame.show();
2005 else this.$editor.show();
2006
2007 if (this.opts.fullpage) this.$editor.attr('contenteditable', true );
2008
2009 this.$source.off('keydown.redactor-textarea-indenting');
2010
2011 this.$editor.focus();
2012 this.selectionRestore();
2013
2014 this.observeStart();
2015 this.buttonActiveVisual();
2016 this.buttonInactive('html');
2017 this.opts.visual = true;
2018 },
2019 toggleCode: function(direct)
2020 {
2021 if (direct !== false) this.selectionSave();
2022
2023 var height = null;
2024 if (this.opts.iframe)
2025 {
2026 height = this.$frame.height();
2027 if (this.opts.fullpage) this.$editor.removeAttr('contenteditable');
2028 this.$frame.hide();
2029 }
2030 else
2031 {
2032 height = this.$editor.innerHeight();
2033 this.$editor.hide();
2034 }
2035
2036 var html = this.$source.val();
2037
2038 // tidy html
2039 if (html !== '' && this.opts.tidyHtml)
2040 {
2041 this.$source.val(this.cleanHtml(html));
2042 }
2043
2044 this.modified = html;
2045
2046 this.$source.height(height).show().focus();
2047
2048 // textarea indenting
2049 this.$source.on('keydown.redactor-textarea-indenting', this.textareaIndenting);
2050
2051 this.buttonInactiveVisual();
2052 this.buttonActive('html');
2053 this.opts.visual = false;
2054 },
2055 textareaIndenting: function(e)
2056 {
2057 if (e.keyCode === 9)
2058 {
2059 var $el = $(this);
2060 var start = $el.get(0).selectionStart;
2061 $el.val($el.val().substring(0, start) + "\t" + $el.val().substring($el.get(0).selectionEnd));
2062 $el.get(0).selectionStart = $el.get(0).selectionEnd = start + 1;
2063 return false;
2064 }
2065 },
2066
2067 // AUTOSAVE
2068 autosave: function()
2069 {
2070 var savedHtml = false;
2071 this.autosaveInterval = setInterval($.proxy(function()
2072 {
2073 var html = this.get();
2074 if (savedHtml !== html)
2075 {
2076 var name = this.$source.attr('name');
2077 $.ajax({
2078 url: this.opts.autosave,
2079 type: 'post',
2080 data: 'name=' + name + '&' + name + '=' + escape(encodeURIComponent(html)),
2081 success: $.proxy(function(data)
2082 {
2083 var json = $.parseJSON(data);
2084 if (typeof json.error == 'undefined')
2085 {
2086 // success
2087 this.callback('autosave', false, json);
2088 }
2089 else
2090 {
2091 // error
2092 this.callback('autosaveError', false, json);
2093 }
2094
2095 savedHtml = html;
2096
2097 }, this)
2098 });
2099 }
2100 }, this), this.opts.autosaveInterval*1000);
2101 },
2102
2103 // TOOLBAR
2104 toolbarBuild: function()
2105 {
2106 // hide on mobile
2107 if (this.isMobile() && this.opts.buttonsHideOnMobile.length > 0)
2108 {
2109 $.each(this.opts.buttonsHideOnMobile, $.proxy(function(i, s)
2110 {
2111 var index = this.opts.buttons.indexOf(s);
2112 this.opts.buttons.splice(index, 1);
2113
2114 }, this));
2115 }
2116
2117 // extend buttons
2118 if (this.opts.air)
2119 {
2120 this.opts.buttons = this.opts.airButtons;
2121 }
2122 else
2123 {
2124 if (!this.opts.buttonSource)
2125 {
2126 var index = this.opts.buttons.indexOf('html');
2127 this.opts.buttons.splice(index, 1);
2128 }
2129 }
2130
2131 // formatting tags
2132 if (this.opts.toolbar)
2133 {
2134 $.each(this.opts.toolbar.formatting.dropdown, $.proxy(function (i, s)
2135 {
2136 if ($.inArray(i, this.opts.formattingTags ) == '-1') delete this.opts.toolbar.formatting.dropdown[i];
2137
2138 }, this));
2139 }
2140
2141 // if no buttons don't create a toolbar
2142 if (this.opts.buttons.length === 0) return false;
2143
2144 // air enable
2145 this.airEnable();
2146
2147 // toolbar build
2148 this.$toolbar = $('<ul>').addClass('redactor_toolbar').attr('id', 'redactor_toolbar_' + this.uuid);
2149
2150 if (this.opts.typewriter)
2151 {
2152 this.$toolbar.addClass('redactor-toolbar-typewriter');
2153 }
2154
2155 if (this.opts.toolbarOverflow && this.isMobile())
2156 {
2157 this.$toolbar.addClass('redactor-toolbar-overflow');
2158 }
2159
2160 if (this.opts.air)
2161 {
2162 // air box
2163 this.$air = $('<div class="redactor_air">').attr('id', 'redactor_air_' + this.uuid).hide();
2164 this.$air.append(this.$toolbar);
2165 $('body').append(this.$air);
2166 }
2167 else
2168 {
2169 if (this.opts.toolbarExternal)
2170 {
2171 this.$toolbar.addClass('redactor-toolbar-external');
2172 $(this.opts.toolbarExternal).html(this.$toolbar);
2173 }
2174 else this.$box.prepend(this.$toolbar);
2175 }
2176
2177 $.each(this.opts.buttons, $.proxy(function(i, btnName)
2178 {
2179 if (this.opts.toolbar[btnName])
2180 {
2181 var btnObject = this.opts.toolbar[btnName];
2182 if (this.opts.fileUpload === false && btnName === 'file') return true;
2183 this.$toolbar.append( $('<li>').append(this.buttonBuild(btnName, btnObject)));
2184 }
2185
2186 }, this));
2187
2188 this.$toolbar.find('a').attr('tabindex', '-1');
2189
2190 // fixed
2191 if (this.opts.toolbarFixed)
2192 {
2193 this.toolbarObserveScroll();
2194 $(this.opts.toolbarFixedTarget).on('scroll.redactor', $.proxy(this.toolbarObserveScroll, this));
2195 }
2196
2197 // buttons response
2198 if (this.opts.activeButtons)
2199 {
2200 this.$editor.on('mouseup.redactor keyup.redactor', $.proxy(this.buttonActiveObserver, this));
2201 }
2202 },
2203 toolbarObserveScroll: function()
2204 {
2205 var scrollTop = $(this.opts.toolbarFixedTarget).scrollTop();
2206
2207 var boxTop = 0;
2208 var left = 0;
2209 var end = 0;
2210
2211 if (this.opts.toolbarFixedTarget === document)
2212 {
2213 boxTop = this.$box.offset().top;
2214 }
2215 else
2216 {
2217 boxTop = 1;
2218 }
2219
2220 end = boxTop + this.$box.height() + 40;
2221
2222 if (scrollTop > boxTop)
2223 {
2224 var width = '100%';
2225 if (this.opts.toolbarFixedBox)
2226 {
2227 left = this.$box.offset().left;
2228 width = this.$box.innerWidth();
2229 this.$toolbar.addClass('toolbar_fixed_box');
2230 }
2231
2232 this.toolbarFixed = true;
2233
2234 if (this.opts.toolbarFixedTarget === document)
2235 {
2236 this.$toolbar.css({
2237 position: 'fixed',
2238 width: width,
2239 zIndex: 10005,
2240 top: this.opts.toolbarFixedTopOffset + 'px',
2241 left: left
2242 });
2243 }
2244 else
2245 {
2246 this.$toolbar.css({
2247 position: 'absolute',
2248 width: width,
2249 zIndex: 10005,
2250 top: (this.opts.toolbarFixedTopOffset + scrollTop) + 'px',
2251 left: 0
2252 });
2253 }
2254
2255 if (scrollTop < end) this.$toolbar.css('visibility', 'visible');
2256 else this.$toolbar.css('visibility', 'hidden');
2257 }
2258 else
2259 {
2260 this.toolbarFixed = false;
2261 this.$toolbar.css({
2262 position: 'relative',
2263 width: 'auto',
2264 top: 0,
2265 left: left
2266 });
2267
2268 if (this.opts.toolbarFixedBox) this.$toolbar.removeClass('toolbar_fixed_box');
2269 }
2270 },
2271
2272 // AIR
2273 airEnable: function()
2274 {
2275 if (!this.opts.air) return;
2276
2277 this.$editor.on('mouseup.redactor keyup.redactor', this, $.proxy(function(e)
2278 {
2279 var text = this.getSelectionText();
2280
2281 if (e.type === 'mouseup' && text != '') this.airShow(e);
2282 if (e.type === 'keyup' && e.shiftKey && text != '')
2283 {
2284 var $focusElem = $(this.getElement(this.getSelection().focusNode)), offset = $focusElem.offset();
2285 offset.height = $focusElem.height();
2286 this.airShow(offset, true);
2287 }
2288
2289 }, this));
2290 },
2291 airShow: function (e, keyboard)
2292 {
2293 if (!this.opts.air) return;
2294
2295 var left, top;
2296 $('.redactor_air').hide();
2297
2298 if (keyboard)
2299 {
2300 left = e.left;
2301 top = e.top + e.height + 14;
2302
2303 if (this.opts.iframe)
2304 {
2305 top += this.$box.position().top - $(this.document).scrollTop();
2306 left += this.$box.position().left;
2307 }
2308 }
2309 else
2310 {
2311 var width = this.$air.innerWidth();
2312
2313 left = e.clientX;
2314 if ($(this.document).width() < (left + width)) left -= width;
2315
2316 top = e.clientY + 14;
2317 if (this.opts.iframe)
2318 {
2319 top += this.$box.position().top;
2320 left += this.$box.position().left;
2321 }
2322 else top += $( this.document ).scrollTop();
2323 }
2324
2325 this.$air.css({
2326 left: left + 'px',
2327 top: top + 'px'
2328 }).show();
2329
2330 this.airBindHide();
2331 },
2332 airBindHide: function()
2333 {
2334 if (!this.opts.air) return;
2335
2336 var hideHandler = $.proxy(function(doc)
2337 {
2338 $(doc).on('mousedown.redactor', $.proxy(function(e)
2339 {
2340 if ($( e.target ).closest(this.$toolbar).length === 0)
2341 {
2342 this.$air.fadeOut(100);
2343 this.selectionRemove();
2344 $(doc).off(e);
2345 }
2346
2347 }, this)).on('keydown.redactor', $.proxy(function(e)
2348 {
2349 if (e.which === this.keyCode.ESC)
2350 {
2351 this.getSelection().collapseToStart();
2352 }
2353
2354 this.$air.fadeOut(100);
2355 $(doc).off(e);
2356
2357 }, this));
2358 }, this);
2359
2360 // Hide the toolbar at events in all documents (iframe)
2361 hideHandler(document);
2362 if (this.opts.iframe) hideHandler(this.document);
2363 },
2364 airBindMousemoveHide: function()
2365 {
2366 if (!this.opts.air) return;
2367
2368 var hideHandler = $.proxy(function(doc)
2369 {
2370 $(doc).on('mousemove.redactor', $.proxy(function(e)
2371 {
2372 if ($( e.target ).closest(this.$toolbar).length === 0)
2373 {
2374 this.$air.fadeOut(100);
2375 $(doc).off(e);
2376 }
2377
2378 }, this));
2379 }, this);
2380
2381 // Hide the toolbar at events in all documents (iframe)
2382 hideHandler(document);
2383 if (this.opts.iframe) hideHandler(this.document);
2384 },
2385
2386 // DROPDOWNS
2387 dropdownBuild: function($dropdown, dropdownObject)
2388 {
2389 $.each(dropdownObject, $.proxy(function(btnName, btnObject)
2390 {
2391 if (!btnObject.className) btnObject.className = '';
2392
2393 var $item;
2394 if (btnObject.name === 'separator') $item = $('<a class="redactor_separator_drop">');
2395 else
2396 {
2397 $item = $('<a href="#" class="' + btnObject.className + ' redactor_dropdown_' + btnName + '">' + btnObject.title + '</a>');
2398 $item.on('click', $.proxy(function(e)
2399 {
2400 if (e.preventDefault) e.preventDefault();
2401 if (this.browser('msie')) e.returnValue = false;
2402
2403 if (btnObject.callback) btnObject.callback.call(this, btnName, $item, btnObject, e);
2404 if (btnObject.exec) this.execCommand(btnObject.exec, btnName);
2405 if (btnObject.func) this[btnObject.func](btnName);
2406
2407 this.buttonActiveObserver();
2408 if (this.opts.air) this.$air.fadeOut(100);
2409
2410 }, this));
2411 }
2412
2413 $dropdown.append($item);
2414
2415 }, this));
2416 },
2417 dropdownShow: function(e, key)
2418 {
2419 if (!this.opts.visual)
2420 {
2421 e.preventDefault();
2422 return false;
2423 }
2424
2425 var $dropdown = this.$toolbar.find('.redactor_dropdown_box_' + key);
2426 var $button = this.buttonGet(key);
2427
2428 if ($button.hasClass('dropact')) this.dropdownHideAll();
2429 else
2430 {
2431 this.dropdownHideAll();
2432 this.callback('dropdownShow', { dropdown: $dropdown, key: key, button: $button });
2433
2434 this.buttonActive(key);
2435 $button.addClass('dropact');
2436
2437 var keyPosition = $button.position();
2438 if (this.toolbarFixed)
2439 {
2440 keyPosition = $button.offset();
2441 }
2442
2443 // fix right placement
2444 var dropdownWidth = $dropdown.width();
2445 if ((keyPosition.left + dropdownWidth) > $(document).width())
2446 {
2447 keyPosition.left -= dropdownWidth;
2448 }
2449
2450 var left = keyPosition.left + 'px';
2451 var btnHeight = $button.innerHeight();
2452
2453 var position = 'absolute';
2454 var top = btnHeight + 'px';
2455
2456 if (this.opts.toolbarFixed && this.toolbarFixed) position = 'fixed';
2457 else if (!this.opts.air) top = keyPosition.top + btnHeight + 'px';
2458
2459 $dropdown.css({ position: position, left: left, top: top }).show();
2460 this.callback('dropdownShown', { dropdown: $dropdown, key: key, button: $button });
2461 }
2462
2463
2464 var hdlHideDropDown = $.proxy(function(e)
2465 {
2466 this.dropdownHide(e, $dropdown);
2467
2468 }, this);
2469
2470 $(document).one('click', hdlHideDropDown);
2471 this.$editor.one('click', hdlHideDropDown);
2472
2473 e.stopPropagation();
2474 this.focusWithSaveScroll();
2475 },
2476 dropdownHideAll: function()
2477 {
2478 this.$toolbar.find('a.dropact').removeClass('redactor_act').removeClass('dropact');
2479 $('.redactor_dropdown').hide();
2480 this.callback('dropdownHide');
2481 },
2482 dropdownHide: function (e, $dropdown)
2483 {
2484 if (!$(e.target).hasClass('dropact'))
2485 {
2486 $dropdown.removeClass('dropact');
2487 this.dropdownHideAll();
2488 }
2489 },
2490
2491 // BUTTONS
2492 buttonBuild: function(btnName, btnObject, buttonImage)
2493 {
2494 var $button = $('<a href="javascript:;" title="' + btnObject.title + '" tabindex="-1" class="re-icon re-' + btnName + '"></a>');
2495
2496 if (typeof buttonImage != 'undefined')
2497 {
2498 $button.addClass('redactor-btn-image');
2499 }
2500
2501 $button.on('click', $.proxy(function(e)
2502 {
2503 if (e.preventDefault) e.preventDefault();
2504 if (this.browser('msie')) e.returnValue = false;
2505
2506 if ($button.hasClass('redactor_button_disabled')) return false;
2507
2508 if (this.isFocused() === false && !btnObject.exec)
2509 {
2510 this.focusWithSaveScroll();
2511 }
2512
2513 if (btnObject.exec)
2514 {
2515 this.focusWithSaveScroll();
2516
2517 this.execCommand(btnObject.exec, btnName);
2518 this.airBindMousemoveHide();
2519
2520 }
2521 else if (btnObject.func && btnObject.func !== 'show')
2522 {
2523 this[btnObject.func](btnName);
2524 this.airBindMousemoveHide();
2525
2526 }
2527 else if (btnObject.callback)
2528 {
2529 btnObject.callback.call(this, btnName, $button, btnObject, e);
2530 this.airBindMousemoveHide();
2531
2532 }
2533 else if (btnObject.dropdown)
2534 {
2535 this.dropdownShow(e, btnName);
2536 }
2537
2538 this.buttonActiveObserver(false, btnName);
2539
2540 }, this));
2541
2542 // dropdown
2543 if (btnObject.dropdown)
2544 {
2545 var $dropdown = $('<div class="redactor_dropdown redactor_dropdown_box_' + btnName + '" style="display: none;">');
2546 $dropdown.appendTo(this.$toolbar);
2547 this.dropdownBuild($dropdown, btnObject.dropdown);
2548 }
2549
2550 return $button;
2551 },
2552 buttonGet: function(key)
2553 {
2554 if (!this.opts.toolbar) return false;
2555 return $(this.$toolbar.find('a.re-' + key));
2556 },
2557 buttonTagToActiveState: function(buttonName, tagName)
2558 {
2559 this.opts.activeButtons.push(buttonName);
2560 this.opts.activeButtonsStates[tagName] = buttonName;
2561 },
2562 buttonActiveToggle: function(key)
2563 {
2564 var btn = this.buttonGet(key);
2565
2566 if (btn.hasClass('redactor_act'))
2567 {
2568 this.buttonInactive(key);
2569 }
2570 else
2571 {
2572 this.buttonActive(key);
2573 }
2574 },
2575 buttonActive: function(key)
2576 {
2577 var btn = this.buttonGet(key);
2578 btn.addClass('redactor_act');
2579 },
2580 buttonInactive: function(key)
2581 {
2582 var btn = this.buttonGet(key);
2583 btn.removeClass('redactor_act');
2584 },
2585 buttonInactiveAll: function(btnName)
2586 {
2587 this.$toolbar.find('a.re-icon').not('.re-' + btnName).removeClass('redactor_act');
2588 },
2589 buttonActiveVisual: function()
2590 {
2591 this.$toolbar.find('a.re-icon').not('a.re-html').removeClass('redactor_button_disabled');
2592 },
2593 buttonInactiveVisual: function()
2594 {
2595 this.$toolbar.find('a.re-icon').not('a.re-html').addClass('redactor_button_disabled');
2596 },
2597 buttonChangeIcon: function (key, classname)
2598 {
2599 this.buttonGet(key).addClass('re-' + classname);
2600 },
2601 buttonRemoveIcon: function(key, classname)
2602 {
2603 this.buttonGet(key).removeClass('re-' + classname);
2604 },
2605 buttonAwesome: function(key, name)
2606 {
2607 var button = this.buttonGet(key);
2608 button.removeClass('redactor-btn-image');
2609 button.addClass('fa-redactor-btn');
2610 button.html('<i class="fa ' + name + '"></i>');
2611 },
2612 buttonAdd: function(key, title, callback, dropdown)
2613 {
2614 if (!this.opts.toolbar) return;
2615 var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true);
2616
2617 this.$toolbar.append($('<li>').append(btn));
2618
2619 return btn;
2620 },
2621 buttonAddFirst: function(key, title, callback, dropdown)
2622 {
2623 if (!this.opts.toolbar) return;
2624 var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true);
2625 this.$toolbar.prepend($('<li>').append(btn));
2626 },
2627 buttonAddAfter: function(afterkey, key, title, callback, dropdown)
2628 {
2629 if (!this.opts.toolbar) return;
2630 var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true);
2631 var $btn = this.buttonGet(afterkey);
2632
2633 if ($btn.size() !== 0) $btn.parent().after($('<li>').append(btn));
2634 else this.$toolbar.append($('<li>').append(btn));
2635
2636 return btn;
2637 },
2638 buttonAddBefore: function(beforekey, key, title, callback, dropdown)
2639 {
2640 if (!this.opts.toolbar) return;
2641 var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true);
2642 var $btn = this.buttonGet(beforekey);
2643
2644 if ($btn.size() !== 0) $btn.parent().before($('<li>').append(btn));
2645 else this.$toolbar.append($('<li>').append(btn));
2646
2647 return btn;
2648 },
2649 buttonRemove: function (key)
2650 {
2651 var $btn = this.buttonGet(key);
2652 $btn.remove();
2653 },
2654 buttonActiveObserver: function(e, btnName)
2655 {
2656 var parent = this.getParent();
2657 this.buttonInactiveAll(btnName);
2658
2659 if (e === false && btnName !== 'html')
2660 {
2661 if ($.inArray(btnName, this.opts.activeButtons) != -1)
2662 {
2663 this.buttonActiveToggle(btnName);
2664 }
2665 return;
2666 }
2667
2668 if (parent && parent.tagName === 'A') this.$toolbar.find('a.redactor_dropdown_link').text(this.opts.curLang.link_edit);
2669 else this.$toolbar.find('a.redactor_dropdown_link').text(this.opts.curLang.link_insert);
2670
2671 $.each(this.opts.activeButtonsStates, $.proxy(function(key, value)
2672 {
2673 if ($(parent).closest(key, this.$editor.get()[0]).length != 0)
2674 {
2675 this.buttonActive(value);
2676 }
2677
2678 }, this));
2679
2680 var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]);
2681 if ($parent.length)
2682 {
2683 var align = $parent.css('text-align');
2684
2685 switch (align)
2686 {
2687 case 'right':
2688 this.buttonActive('alignright');
2689 break;
2690 case 'center':
2691 this.buttonActive('aligncenter');
2692 break;
2693 case 'justify':
2694 this.buttonActive('alignjustify');
2695 break;
2696 default:
2697 this.buttonActive('alignleft');
2698 break;
2699 }
2700 }
2701 },
2702
2703 // EXEC
2704 execPasteFrag: function(html)
2705 {
2706 var sel = this.getSelection();
2707 if (sel.getRangeAt && sel.rangeCount)
2708 {
2709 var range = this.getRange();
2710 range.deleteContents();
2711
2712 var el = this.document.createElement("div");
2713 el.innerHTML = html;
2714
2715 var frag = this.document.createDocumentFragment(), node, lastNode;
2716 while ((node = el.firstChild))
2717 {
2718 lastNode = frag.appendChild(node);
2719 }
2720
2721 var firstNode = frag.firstChild;
2722 range.insertNode(frag);
2723
2724 if (lastNode)
2725 {
2726 range = range.cloneRange();
2727 range.setStartAfter(lastNode);
2728 range.collapse(true);
2729 }
2730 sel.removeAllRanges();
2731 sel.addRange(range);
2732 }
2733 },
2734 exec: function(cmd, param, sync)
2735 {
2736 if (cmd === 'formatblock' && this.browser('msie'))
2737 {
2738 param = '<' + param + '>';
2739 }
2740
2741 if (cmd === 'inserthtml' && this.browser('msie'))
2742 {
2743 if (!this.isIe11())
2744 {
2745 this.focusWithSaveScroll();
2746 this.document.selection.createRange().pasteHTML(param);
2747 }
2748 else this.execPasteFrag(param);
2749 }
2750 else
2751 {
2752 this.document.execCommand(cmd, false, param);
2753 }
2754
2755 if (sync !== false) this.sync();
2756 this.callback('execCommand', cmd, param);
2757 },
2758 execCommand: function(cmd, param, sync)
2759 {
2760 if (!this.opts.visual)
2761 {
2762 this.$source.focus();
2763 return false;
2764 }
2765
2766 if ( cmd === 'bold'
2767 || cmd === 'italic'
2768 || cmd === 'underline'
2769 || cmd === 'strikethrough')
2770 {
2771 this.bufferSet();
2772 }
2773
2774
2775 if (cmd === 'superscript' || cmd === 'subscript')
2776 {
2777 var parent = this.getParent();
2778 if (parent.tagName === 'SUP' || parent.tagName === 'SUB')
2779 {
2780 this.inlineRemoveFormatReplace(parent);
2781 }
2782 }
2783
2784 if (cmd === 'inserthtml')
2785 {
2786 this.insertHtml(param, sync);
2787 this.callback('execCommand', cmd, param);
2788 return;
2789 }
2790
2791 // Stop formatting pre
2792 if (this.currentOrParentIs('PRE') && !this.opts.formattingPre) return false;
2793
2794 // Lists
2795 if (cmd === 'insertunorderedlist' || cmd === 'insertorderedlist') return this.execLists(cmd, param);
2796
2797 // Unlink
2798 if (cmd === 'unlink') return this.execUnlink(cmd, param);
2799
2800 // Usual exec
2801 this.exec(cmd, param, sync);
2802
2803 // Line
2804 if (cmd === 'inserthorizontalrule') this.$editor.find('hr').removeAttr('id');
2805
2806 },
2807 execUnlink: function(cmd, param)
2808 {
2809 this.bufferSet();
2810
2811 var link = this.currentOrParentIs('A');
2812 if (link)
2813 {
2814 $(link).replaceWith($(link).text());
2815
2816 this.sync();
2817 this.callback('execCommand', cmd, param);
2818 return;
2819 }
2820 },
2821 execLists: function(cmd, param)
2822 {
2823 this.bufferSet();
2824
2825 var parent = this.getParent();
2826 var $list = $(parent).closest('ol, ul');
2827
2828 if (!this.isParentRedactor($list) && $list.size() != 0)
2829 {
2830 $list = false;
2831 }
2832
2833 var remove = false;
2834
2835 if ($list && $list.length)
2836 {
2837 remove = true;
2838 var listTag = $list[0].tagName;
2839 if ((cmd === 'insertunorderedlist' && listTag === 'OL')
2840 || (cmd === 'insertorderedlist' && listTag === 'UL'))
2841 {
2842 remove = false;
2843 }
2844 }
2845
2846 this.selectionSave();
2847
2848 // remove lists
2849 if (remove)
2850 {
2851
2852 var nodes = this.getNodes();
2853 var elems = this.getBlocks(nodes);
2854
2855 if (typeof nodes[0] != 'undefined' && nodes.length > 1 && nodes[0].nodeType == 3)
2856 {
2857 // fix the adding the first li to the array
2858 elems.unshift(this.getBlock());
2859 }
2860
2861 var data = '', replaced = '';
2862 $.each(elems, $.proxy(function(i,s)
2863 {
2864 if (s.tagName == 'LI')
2865 {
2866 var $s = $(s);
2867 var cloned = $s.clone();
2868 cloned.find('ul', 'ol').remove();
2869
2870 if (this.opts.linebreaks === false)
2871 {
2872 data += this.outerHtml($('<p>').append(cloned.contents()));
2873 }
2874 else
2875 {
2876 data += cloned.html() + '<br>';
2877 }
2878
2879 if (i == 0)
2880 {
2881 $s.addClass('redactor-replaced').empty();
2882 replaced = this.outerHtml($s);
2883 }
2884 else $s.remove();
2885 }
2886
2887 }, this));
2888
2889
2890 html = this.$editor.html().replace(replaced, '</' + listTag + '>' + data + '<' + listTag + '>');
2891
2892 this.$editor.html(html);
2893 this.$editor.find(listTag + ':empty').remove();
2894
2895 }
2896
2897 // insert lists
2898 else
2899 {
2900 var firstParent = $(this.getParent()).closest('td');
2901
2902 this.document.execCommand(cmd);
2903
2904 var parent = this.getParent();
2905 var $list = $(parent).closest('ol, ul');
2906
2907 if (firstParent.size() != 0)
2908 {
2909 $list.wrapAll('<td>');
2910 }
2911
2912 if ($list.length)
2913 {
2914 // remove block-element list wrapper
2915 var $listParent = $list.parent();
2916 if (this.isParentRedactor($listParent) && $listParent[0].tagName != 'LI' && this.nodeTestBlocks($listParent[0]))
2917 {
2918 $listParent.replaceWith($listParent.contents());
2919 }
2920 }
2921
2922 if (this.browser('mozilla'))
2923 {
2924 this.$editor.focus();
2925 }
2926 }
2927
2928 this.selectionRestore();
2929
2930 this.sync();
2931 this.callback('execCommand', cmd, param);
2932 return;
2933 },
2934
2935 // INDENTING
2936 indentingIndent: function()
2937 {
2938 this.indentingStart('indent');
2939 },
2940 indentingOutdent: function()
2941 {
2942 this.indentingStart('outdent');
2943 },
2944 indentingStart: function(cmd)
2945 {
2946 this.bufferSet();
2947
2948 if (cmd === 'indent')
2949 {
2950 var block = this.getBlock();
2951
2952 this.selectionSave();
2953
2954 if (block && block.tagName == 'LI')
2955 {
2956 // li
2957 var parent = this.getParent();
2958
2959 var $list = $(parent).closest('ol, ul');
2960 var listTag = $list[0].tagName;
2961
2962 var elems = this.getBlocks();
2963
2964 $.each(elems, function(i,s)
2965 {
2966 if (s.tagName == 'LI')
2967 {
2968 var $prev = $(s).prev();
2969 if ($prev.size() != 0 && $prev[0].tagName == 'LI')
2970 {
2971 var $childList = $prev.children('ul, ol');
2972 if ($childList.size() == 0)
2973 {
2974 $prev.append($('<' + listTag + '>').append(s));
2975 }
2976 else $childList.append(s);
2977 }
2978 }
2979 });
2980 }
2981 // linebreaks
2982 else if (block === false && this.opts.linebreaks === true)
2983 {
2984 this.exec('formatBlock', 'blockquote');
2985 var newblock = this.getBlock();
2986 var block = $('<div data-tagblock="">').html($(newblock).html());
2987 $(newblock).replaceWith(block);
2988
2989 var left = this.normalize($(block).css('margin-left')) + this.opts.indentValue;
2990 $(block).css('margin-left', left + 'px');
2991 }
2992 else
2993 {
2994 // all block tags
2995 var elements = this.getBlocks();
2996 $.each(elements, $.proxy(function(i, elem)
2997 {
2998 var $el = false;
2999
3000 if (elem.tagName === 'TD') return;
3001
3002 if ($.inArray(elem.tagName, this.opts.alignmentTags) !== -1)
3003 {
3004 $el = $(elem);
3005 }
3006 else
3007 {
3008 $el = $(elem).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]);
3009 }
3010
3011 var left = this.normalize($el.css('margin-left')) + this.opts.indentValue;
3012 $el.css('margin-left', left + 'px');
3013
3014 }, this));
3015 }
3016
3017 this.selectionRestore();
3018
3019 }
3020 // outdent
3021 else
3022 {
3023 this.selectionSave();
3024
3025 var block = this.getBlock();
3026 if (block && block.tagName == 'LI')
3027 {
3028 // li
3029 var elems = this.getBlocks();
3030 var index = 0;
3031
3032 this.insideOutdent(block, index, elems);
3033 }
3034 else
3035 {
3036 // all block tags
3037 var elements = this.getBlocks();
3038 $.each(elements, $.proxy(function(i, elem)
3039 {
3040 var $el = false;
3041
3042 if ($.inArray(elem.tagName, this.opts.alignmentTags) !== -1)
3043 {
3044 $el = $(elem);
3045 }
3046 else
3047 {
3048 $el = $(elem).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]);
3049 }
3050
3051 var left = this.normalize($el.css('margin-left')) - this.opts.indentValue;
3052 if (left <= 0)
3053 {
3054 // linebreaks
3055 if (this.opts.linebreaks === true && typeof($el.data('tagblock')) !== 'undefined')
3056 {
3057 $el.replaceWith($el.html());
3058 }
3059 // all block tags
3060 else
3061 {
3062 $el.css('margin-left', '');
3063 this.removeEmptyAttr($el, 'style');
3064 }
3065 }
3066 else
3067 {
3068 $el.css('margin-left', left + 'px');
3069 }
3070
3071 }, this));
3072 }
3073
3074
3075 this.selectionRestore();
3076 }
3077
3078 this.sync();
3079
3080 },
3081 insideOutdent: function (li, index, elems)
3082 {
3083 if (li && li.tagName == 'LI')
3084 {
3085 var $parent = $(li).parent().parent();
3086 if ($parent.size() != 0 && $parent[0].tagName == 'LI')
3087 {
3088 $parent.after(li);
3089 }
3090 else
3091 {
3092 if (typeof elems[index] != 'undefined')
3093 {
3094 li = elems[index];
3095 index++;
3096
3097 this.insideOutdent(li, index, elems);
3098 }
3099 else
3100 {
3101 this.execCommand('insertunorderedlist');
3102 }
3103 }
3104 }
3105 },
3106
3107 // ALIGNMENT
3108 alignmentLeft: function()
3109 {
3110 this.alignmentSet('', 'JustifyLeft');
3111 },
3112 alignmentRight: function()
3113 {
3114 this.alignmentSet('right', 'JustifyRight');
3115 },
3116 alignmentCenter: function()
3117 {
3118 this.alignmentSet('center', 'JustifyCenter');
3119 },
3120 alignmentJustify: function()
3121 {
3122 this.alignmentSet('justify', 'JustifyFull');
3123 },
3124 alignmentSet: function(type, cmd)
3125 {
3126 this.bufferSet();
3127
3128 if (this.oldIE())
3129 {
3130 this.document.execCommand(cmd, false, false);
3131 return true;
3132 }
3133
3134 this.selectionSave();
3135
3136 var block = this.getBlock();
3137 if (!block && this.opts.linebreaks)
3138 {
3139 // one element
3140 this.exec('formatblock', 'div');
3141
3142 var newblock = this.getBlock();
3143 var block = $('<div data-tagblock="">').html($(newblock).html());
3144 $(newblock).replaceWith(block);
3145
3146 $(block).css('text-align', type);
3147 this.removeEmptyAttr(block, 'style');
3148
3149 if (type == '' && typeof($(block).data('tagblock')) !== 'undefined')
3150 {
3151 $(block).replaceWith($(block).html());
3152 }
3153 }
3154 else
3155 {
3156 var elements = this.getBlocks();
3157 $.each(elements, $.proxy(function(i, elem)
3158 {
3159 var $el = false;
3160
3161 if ($.inArray(elem.tagName, this.opts.alignmentTags) !== -1)
3162 {
3163 $el = $(elem);
3164 }
3165 else
3166 {
3167 $el = $(elem).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]);
3168 }
3169
3170 if ($el)
3171 {
3172 $el.css('text-align', type);
3173 this.removeEmptyAttr($el, 'style');
3174 }
3175
3176 }, this));
3177 }
3178
3179 this.selectionRestore();
3180
3181 this.sync();
3182 },
3183
3184 // CLEAN
3185 cleanEmpty: function(html)
3186 {
3187 var ph = this.placeholderStart(html);
3188 if (ph !== false) return ph;
3189
3190 if (this.opts.linebreaks === false)
3191 {
3192 if (html === '') html = this.opts.emptyHtml;
3193 else if (html.search(/^<hr\s?\/?>$/gi) !== -1) html = '<hr>' + this.opts.emptyHtml;
3194 }
3195
3196 return html;
3197 },
3198 cleanConverters: function(html)
3199 {
3200 // convert div to p
3201 if (this.opts.convertDivs) html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '<p$1>$2</p>');
3202 if (this.opts.paragraphy) html = this.cleanParagraphy(html);
3203
3204 return html;
3205 },
3206 cleanConvertProtected: function(html)
3207 {
3208 if (this.opts.templateVars)
3209 {
3210 html = html.replace(/\{\{(.*?)\}\}/gi, '<!-- template double $1 -->');
3211 html = html.replace(/\{(.*?)\}/gi, '<!-- template $1 -->');
3212 }
3213
3214 html = html.replace(/<script(.*?)>([\w\W]*?)<\/script>/gi, '<title type="text/javascript" style="display: none;" class="redactor-script-tag"$1>$2</title>');
3215 html = html.replace(/<style(.*?)>([\w\W]*?)<\/style>/gi, '<section$1 style="display: none;" rel="redactor-style-tag">$2</section>');
3216 html = html.replace(/<form(.*?)>([\w\W]*?)<\/form>/gi, '<section$1 rel="redactor-form-tag">$2</section>');
3217
3218 // php tags convertation
3219 if (this.opts.phpTags) html = html.replace(/<\?php([\w\W]*?)\?>/gi, '<section style="display: none;" rel="redactor-php-tag">$1</section>');
3220 else html = html.replace(/<\?php([\w\W]*?)\?>/gi, '');
3221
3222 return html;
3223 },
3224 cleanReConvertProtected: function(html)
3225 {
3226 if (this.opts.templateVars)
3227 {
3228 html = html.replace(/<!-- template double (.*?) -->/gi, '{{$1}}');
3229 html = html.replace(/<!-- template (.*?) -->/gi, '{$1}');
3230 }
3231
3232 html = html.replace(/<title type="text\/javascript" style="display: none;" class="redactor-script-tag"(.*?)>([\w\W]*?)<\/title>/gi, '<script$1 type="text/javascript">$2</script>');
3233 html = html.replace(/<section(.*?) style="display: none;" rel="redactor-style-tag">([\w\W]*?)<\/section>/gi, '<style$1>$2</style>');
3234 html = html.replace(/<section(.*?)rel="redactor-form-tag"(.*?)>([\w\W]*?)<\/section>/gi, '<form$1$2>$3</form>');
3235
3236 // php tags convertation
3237 if (this.opts.phpTags) html = html.replace(/<section style="display: none;" rel="redactor-php-tag">([\w\W]*?)<\/section>/gi, '<?php\r\n$1\r\n?>');
3238
3239 return html;
3240 },
3241 cleanRemoveSpaces: function(html, buffer)
3242 {
3243 if (buffer !== false)
3244 {
3245 var buffer = []
3246 var matches = html.match(/<(pre|style|script|title)(.*?)>([\w\W]*?)<\/(pre|style|script|title)>/gi);
3247 if (matches === null) matches = [];
3248
3249 if (this.opts.phpTags)
3250 {
3251 var phpMatches = html.match(/<\?php([\w\W]*?)\?>/gi);
3252 if (phpMatches) matches = $.merge(matches, phpMatches);
3253 }
3254
3255 if (matches)
3256 {
3257 $.each(matches, function(i, s)
3258 {
3259 html = html.replace(s, 'buffer_' + i);
3260 buffer.push(s);
3261 });
3262 }
3263 }
3264
3265 html = html.replace(/\n/g, ' ');
3266 html = html.replace(/[\t]*/g, '');
3267 html = html.replace(/\n\s*\n/g, "\n");
3268 html = html.replace(/^[\s\n]*/g, ' ');
3269 html = html.replace(/[\s\n]*$/g, ' ');
3270 html = html.replace( />\s{2,}</g, '> <'); // between inline tags can be only one space
3271
3272 html = this.cleanReplacer(html, buffer);
3273
3274 html = html.replace(/\n\n/g, "\n");
3275
3276 return html;
3277 },
3278 cleanReplacer: function(html, buffer)
3279 {
3280 if (buffer === false) return html;
3281
3282 $.each(buffer, function(i,s)
3283 {
3284 html = html.replace('buffer_' + i, s);
3285 });
3286
3287 return html;
3288 },
3289 cleanRemoveEmptyTags: function(html)
3290 {
3291 html = html.replace(/<span>([\w\W]*?)<\/span>/gi, '$1');
3292
3293 // remove zero width-space
3294 html = html.replace(/[\u200B-\u200D\uFEFF]/g, '');
3295
3296 var etagsInline = ["<b>\\s*</b>", "<b>&nbsp;</b>", "<em>\\s*</em>"]
3297 var etags = ["<pre></pre>", "<blockquote>\\s*</blockquote>", "<dd></dd>", "<dt></dt>", "<ul></ul>", "<ol></ol>", "<li></li>", "<table></table>", "<tr></tr>", "<span>\\s*<span>", "<span>&nbsp;<span>", "<p>\\s*</p>", "<p></p>", "<p>&nbsp;</p>", "<p>\\s*<br>\\s*</p>", "<div>\\s*</div>", "<div>\\s*<br>\\s*</div>"];
3298
3299 if (this.opts.removeEmptyTags)
3300 {
3301 etags = etags.concat(etagsInline);
3302 }
3303 else etags = etagsInline;
3304
3305 var len = etags.length;
3306 for (var i = 0; i < len; ++i)
3307 {
3308 html = html.replace(new RegExp(etags[i], 'gi'), "");
3309 }
3310
3311 return html;
3312 },
3313 cleanParagraphy: function(html)
3314 {
3315 html = $.trim(html);
3316
3317 if (this.opts.linebreaks === true) return html;
3318 if (html === '' || html === '<p></p>') return this.opts.emptyHtml;
3319
3320 html = html + "\n";
3321
3322 if (this.opts.removeEmptyTags === false)
3323 {
3324 return html;
3325 }
3326
3327 var safes = [];
3328 var matches = html.match(/<(table|div|pre|object)(.*?)>([\w\W]*?)<\/(table|div|pre|object)>/gi);
3329 if (!matches) matches = [];
3330
3331 var commentsMatches = html.match(/<!--([\w\W]*?)-->/gi);
3332 if (commentsMatches) matches = $.merge(matches, commentsMatches);
3333
3334 if (this.opts.phpTags)
3335 {
3336 var phpMatches = html.match(/<section(.*?)rel="redactor-php-tag">([\w\W]*?)<\/section>/gi);
3337 if (phpMatches) matches = $.merge(matches, phpMatches);
3338 }
3339
3340 if (matches)
3341 {
3342 $.each(matches, function(i,s)
3343 {
3344 safes[i] = s;
3345 html = html.replace(s, '{replace' + i + '}\n');
3346 });
3347 }
3348
3349 html = html.replace(/<br \/>\s*<br \/>/gi, "\n\n");
3350
3351 function R(str, mod, r)
3352 {
3353 return html.replace(new RegExp(str, mod), r);
3354 }
3355
3356 var blocks = '(comment|html|body|head|title|meta|style|script|link|iframe|table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|option|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary)';
3357
3358 html = R('(<' + blocks + '[^>]*>)', 'gi', "\n$1");
3359 html = R('(</' + blocks + '>)', 'gi', "$1\n\n");
3360 html = R("\r\n", 'g', "\n");
3361 html = R("\r", 'g', "\n");
3362 html = R("/\n\n+/", 'g', "\n\n");
3363
3364 var htmls = html.split(new RegExp('\n\s*\n', 'g'), -1);
3365
3366 html = '';
3367 for (var i in htmls)
3368 {
3369 if (htmls.hasOwnProperty(i))
3370 {
3371 if (htmls[i].search('{replace') == -1)
3372 {
3373 htmls[i] = htmls[i].replace(/<p>\n\t<\/p>/gi, '');
3374 htmls[i] = htmls[i].replace(/<p><\/p>/gi, '');
3375
3376 if (htmls[i] != '')
3377 {
3378 html += '<p>' + htmls[i].replace(/^\n+|\n+$/g, "") + "</p>";
3379 }
3380 }
3381 else html += htmls[i];
3382 }
3383 }
3384
3385 html = R('<p><p>', 'gi', '<p>');
3386 html = R('</p></p>', 'gi', '</p>');
3387
3388 html = R('<p>\s?</p>', 'gi', '');
3389
3390 html = R('<p>([^<]+)</(div|address|form)>', 'gi', "<p>$1</p></$2>");
3391
3392 html = R('<p>(</?' + blocks + '[^>]*>)</p>', 'gi', "$1");
3393 html = R("<p>(<li.+?)</p>", 'gi', "$1");
3394 html = R('<p>\s?(</?' + blocks + '[^>]*>)', 'gi', "$1");
3395
3396 html = R('(</?' + blocks + '[^>]*>)\s?</p>', 'gi', "$1");
3397 html = R('(</?' + blocks + '[^>]*>)\s?<br />', 'gi', "$1");
3398 html = R('<br />(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)[^>]*>)', 'gi', '$1');
3399 html = R("\n</p>", 'gi', '</p>');
3400
3401 html = R('<li><p>', 'gi', '<li>');
3402 html = R('</p></li>', 'gi', '</li>');
3403 html = R('</li><p>', 'gi', '</li>');
3404 //html = R('</ul><p>(.*?)</li>', 'gi', '</ul></li>');
3405 // html = R('</ol><p>', 'gi', '</ol>');
3406 html = R('<p>\t?\n?<p>', 'gi', '<p>');
3407 html = R('</dt><p>', 'gi', '</dt>');
3408 html = R('</dd><p>', 'gi', '</dd>');
3409 html = R('<br></p></blockquote>', 'gi', '</blockquote>');
3410 html = R('<p>\t*</p>', 'gi', '');
3411
3412 // restore safes
3413 $.each(safes, function(i,s)
3414 {
3415 html = html.replace('{replace' + i + '}', s);
3416 });
3417
3418 return $.trim(html);
3419 },
3420 cleanConvertInlineTags: function(html, set)
3421 {
3422 var boldTag = 'strong';
3423 if (this.opts.boldTag === 'b') boldTag = 'b';
3424
3425 var italicTag = 'em';
3426 if (this.opts.italicTag === 'i') italicTag = 'i';
3427
3428 html = html.replace(/<span style="font-style: italic;">([\w\W]*?)<\/span>/gi, '<' + italicTag + '>$1</' + italicTag + '>');
3429 html = html.replace(/<span style="font-weight: bold;">([\w\W]*?)<\/span>/gi, '<' + boldTag + '>$1</' + boldTag + '>');
3430
3431 // bold, italic, del
3432 if (this.opts.boldTag === 'strong') html = html.replace(/<b>([\w\W]*?)<\/b>/gi, '<strong>$1</strong>');
3433 else html = html.replace(/<strong>([\w\W]*?)<\/strong>/gi, '<b>$1</b>');
3434
3435 if (this.opts.italicTag === 'em') html = html.replace(/<i>([\w\W]*?)<\/i>/gi, '<em>$1</em>');
3436 else html = html.replace(/<em>([\w\W]*?)<\/em>/gi, '<i>$1</i>');
3437
3438 if (set !== true)
3439 {
3440 html = html.replace(/<strike>([\w\W]*?)<\/strike>/gi, '<del>$1</del>');
3441 }
3442 else
3443 {
3444 html = html.replace(/<del>([\w\W]*?)<\/del>/gi, '<strike>$1</strike>');
3445 }
3446
3447 return html;
3448 },
3449 cleanStripTags: function(html)
3450 {
3451 if (html == '' || typeof html == 'undefined') return html;
3452
3453 var allowed = false;
3454 if (this.opts.allowedTags !== false) allowed = true;
3455
3456 var arr = allowed === true ? this.opts.allowedTags : this.opts.deniedTags;
3457
3458 var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
3459 html = html.replace(tags, function ($0, $1)
3460 {
3461 if (allowed === true) return $.inArray($1.toLowerCase(), arr) > '-1' ? $0 : '';
3462 else return $.inArray($1.toLowerCase(), arr) > '-1' ? '' : $0;
3463 });
3464
3465 html = this.cleanConvertInlineTags(html);
3466
3467 return html;
3468
3469 },
3470 cleanSavePreCode: function(html, encode)
3471 {
3472 var pre = html.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/gi);
3473 if (pre !== null)
3474 {
3475 $.each(pre, $.proxy(function(i,s)
3476 {
3477 var arr = s.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/i);
3478
3479 arr[3] = arr[3].replace(/&nbsp;/g, ' ');
3480
3481 if (encode !== false) arr[3] = this.cleanEncodeEntities(arr[3]);
3482
3483 // $ fix
3484 arr[3] = arr[3].replace(/\$/g, '&#36;');
3485
3486 html = html.replace(s, '<' + arr[1] + arr[2] + '>' + arr[3] + '</' + arr[1] + '>');
3487
3488 }, this));
3489 }
3490
3491 return html;
3492 },
3493 cleanEncodeEntities: function(str)
3494 {
3495 str = String(str).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
3496 return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3497 },
3498 cleanUnverified: function()
3499 {
3500 // label, abbr, mark, meter, code, q, dfn, ins, time, kbd, var
3501 var $elem = this.$editor.find('li, img, a, b, strong, sub, sup, i, em, u, small, strike, del, span, cite');
3502
3503 $elem.filter('[style*="background-color: transparent;"][style*="line-height"]')
3504 .css('background-color', '')
3505 .css('line-height', '');
3506
3507 $elem.filter('[style*="background-color: transparent;"]')
3508 .css('background-color', '');
3509
3510 $elem.css('line-height', '');
3511
3512 $.each($elem, $.proxy(function(i,s)
3513 {
3514 this.removeEmptyAttr(s, 'style');
3515 }, this));
3516
3517 // When we paste text in Safari is wrapping inserted div (remove it)
3518 this.$editor.find('div[style="text-align: -webkit-auto;"]').contents().unwrap();
3519
3520 // Remove all styles in ul, ol, li
3521 this.$editor.find('ul, ol, li').removeAttr('style');
3522 },
3523
3524
3525 // TEXTAREA CODE FORMATTING
3526 cleanHtml: function(code)
3527 {
3528 var i = 0,
3529 codeLength = code.length,
3530 point = 0,
3531 start = null,
3532 end = null,
3533 tag = '',
3534 out = '',
3535 cont = '';
3536
3537 this.cleanlevel = 0;
3538
3539 for (; i < codeLength; i++)
3540 {
3541 point = i;
3542
3543 // if no more tags, copy and exit
3544 if (-1 == code.substr(i).indexOf( '<' ))
3545 {
3546 out += code.substr(i);
3547
3548 return this.cleanFinish(out);
3549 }
3550
3551 // copy verbatim until a tag
3552 while (point < codeLength && code.charAt(point) != '<')
3553 {
3554 point++;
3555 }
3556
3557 if (i != point)
3558 {
3559 cont = code.substr(i, point - i);
3560 if (!cont.match(/^\s{2,}$/g))
3561 {
3562 if ('\n' == out.charAt(out.length - 1)) out += this.cleanGetTabs();
3563 else if ('\n' == cont.charAt(0))
3564 {
3565 out += '\n' + this.cleanGetTabs();
3566 cont = cont.replace(/^\s+/, '');
3567 }
3568
3569 out += cont;
3570 }
3571
3572 if (cont.match(/\n/)) out += '\n' + this.cleanGetTabs();
3573 }
3574
3575 start = point;
3576
3577 // find the end of the tag
3578 while (point < codeLength && '>' != code.charAt(point))
3579 {
3580 point++;
3581 }
3582
3583 tag = code.substr(start, point - start);
3584 i = point;
3585
3586 var t;
3587
3588 if ('!--' == tag.substr(1, 3))
3589 {
3590 if (!tag.match(/--$/))
3591 {
3592 while ('-->' != code.substr(point, 3))
3593 {
3594 point++;
3595 }
3596 point += 2;
3597 tag = code.substr(start, point - start);
3598 i = point;
3599 }
3600
3601 if ('\n' != out.charAt(out.length - 1)) out += '\n';
3602
3603 out += this.cleanGetTabs();
3604 out += tag + '>\n';
3605 }
3606 else if ('!' == tag[1])
3607 {
3608 out = this.placeTag(tag + '>', out);
3609 }
3610 else if ('?' == tag[1])
3611 {
3612 out += tag + '>\n';
3613 }
3614 else if (t = tag.match(/^<(script|style|pre)/i))
3615 {
3616 t[1] = t[1].toLowerCase();
3617 tag = this.cleanTag(tag);
3618 out = this.placeTag(tag, out);
3619 end = String(code.substr(i + 1)).toLowerCase().indexOf('</' + t[1]);
3620
3621 if (end)
3622 {
3623 cont = code.substr(i + 1, end);
3624 i += end;
3625 out += cont;
3626 }
3627 }
3628 else
3629 {
3630 tag = this.cleanTag(tag);
3631 out = this.placeTag(tag, out);
3632 }
3633 }
3634
3635 return this.cleanFinish( out );
3636 },
3637 cleanGetTabs: function()
3638 {
3639 var s = '';
3640 for ( var j = 0; j < this.cleanlevel; j++ )
3641 {
3642 s += '\t';
3643 }
3644
3645 return s;
3646 },
3647 cleanFinish: function(code)
3648 {
3649 code = code.replace( /\n\s*\n/g, '\n' );
3650 code = code.replace( /^[\s\n]*/, '' );
3651 code = code.replace( /[\s\n]*$/, '' );
3652 code = code.replace( /<script(.*?)>\n<\/script>/gi, '<script$1></script>' );
3653
3654 this.cleanlevel = 0;
3655
3656 return code;
3657 },
3658 cleanTag: function (tag)
3659 {
3660 var tagout = '';
3661 tag = tag.replace(/\n/g, ' ');
3662 tag = tag.replace(/\s{2,}/g, ' ');
3663 tag = tag.replace(/^\s+|\s+$/g, ' ');
3664
3665 var suffix = '';
3666 if (tag.match(/\/$/))
3667 {
3668 suffix = '/';
3669 tag = tag.replace(/\/+$/, '');
3670 }
3671
3672 var m;
3673 while (m = /\s*([^= ]+)(?:=((['"']).*?\3|[^ ]+))?/.exec(tag))
3674 {
3675 if (m[2]) tagout += m[1].toLowerCase() + '=' + m[2];
3676 else if (m[1]) tagout += m[1].toLowerCase();
3677
3678 tagout += ' ';
3679 tag = tag.substr(m[0].length);
3680 }
3681
3682 return tagout.replace(/\s*$/, '') + suffix + '>';
3683 },
3684 placeTag: function (tag, out)
3685 {
3686 var nl = tag.match(this.cleannewLevel);
3687 if (tag.match(this.cleanlineBefore) || nl)
3688 {
3689 out = out.replace(/\s*$/, '');
3690 out += '\n';
3691 }
3692
3693 if (nl && '/' == tag.charAt(1)) this.cleanlevel--;
3694 if ('\n' == out.charAt(out.length - 1)) out += this.cleanGetTabs();
3695 if (nl && '/' != tag.charAt(1)) this.cleanlevel++;
3696
3697 out += tag;
3698
3699 if (tag.match(this.cleanlineAfter) || tag.match(this.cleannewLevel))
3700 {
3701 out = out.replace(/ *$/, '');
3702 out += '\n';
3703 }
3704
3705 return out;
3706 },
3707
3708 // FORMAT
3709 formatEmpty: function(e)
3710 {
3711 var html = $.trim(this.$editor.html());
3712
3713 if (this.opts.linebreaks)
3714 {
3715 if (html == '')
3716 {
3717 e.preventDefault();
3718 this.$editor.html('');
3719 this.focus();
3720 }
3721 }
3722 else
3723 {
3724 html = html.replace(/<br\s?\/?>/i, '');
3725 var thtml = html.replace(/<p>\s?<\/p>/gi, '');
3726
3727 if (html === '' || thtml === '')
3728 {
3729 e.preventDefault();
3730
3731 var node = $(this.opts.emptyHtml).get(0);
3732 this.$editor.html(node);
3733 this.focus();
3734 }
3735 }
3736
3737 this.sync();
3738 },
3739 formatBlocks: function(tag)
3740 {
3741 this.bufferSet();
3742
3743 var nodes = this.getBlocks();
3744 this.selectionSave();
3745
3746 $.each(nodes, $.proxy(function(i, node)
3747 {
3748 if (node.tagName !== 'LI')
3749 {
3750 var parent = $(node).parent();
3751
3752 if (tag === 'p')
3753 {
3754 if ((node.tagName === 'P'
3755 && parent.size() != 0
3756 && parent[0].tagName === 'BLOCKQUOTE')
3757 ||
3758 node.tagName === 'BLOCKQUOTE')
3759 {
3760 this.formatQuote();
3761 return;
3762 }
3763 else if (this.opts.linebreaks)
3764 {
3765 if (node && node.tagName.search(/H[1-6]/) == 0)
3766 {
3767 $(node).replaceWith(node.innerHTML + '<br>');
3768 }
3769 else return;
3770 }
3771 else
3772 {
3773 this.formatBlock(tag, node);
3774 }
3775 }
3776 else
3777 {
3778 this.formatBlock(tag, node);
3779 }
3780 }
3781
3782 }, this));
3783
3784 this.selectionRestore();
3785 this.sync();
3786 },
3787 formatBlock: function(tag, block)
3788 {
3789 if (block === false) block = this.getBlock();
3790 if (block === false && this.opts.linebreaks === true)
3791 {
3792 this.execCommand('formatblock', tag);
3793 return true;
3794 }
3795
3796 var contents = '';
3797 if (tag !== 'pre')
3798 {
3799 contents = $(block).contents();
3800 }
3801 else
3802 {
3803 //contents = this.cleanEncodeEntities($(block).text());
3804 contents = $(block).html();
3805 if ($.trim(contents) === '')
3806 {
3807 contents = '<span id="selection-marker-1"></span>';
3808 }
3809 }
3810
3811 if (block.tagName === 'PRE') tag = 'p';
3812
3813 if (this.opts.linebreaks === true && tag === 'p')
3814 {
3815 $(block).replaceWith($('<div>').append(contents).html() + '<br>');
3816 }
3817 else
3818 {
3819 var parent = this.getParent();
3820
3821 var node = $('<' + tag + '>').append(contents);
3822 $(block).replaceWith(node);
3823
3824 if (parent && parent.tagName == 'TD')
3825 {
3826 $(node).wrapAll('<td>');
3827 }
3828 }
3829 },
3830 formatChangeTag: function(fromElement, toTagName, save)
3831 {
3832 if (save !== false) this.selectionSave();
3833
3834 var newElement = $('<' + toTagName + '/>');
3835 $(fromElement).replaceWith(function() { return newElement.append($(this).contents()); });
3836
3837 if (save !== false) this.selectionRestore();
3838
3839 return newElement;
3840 },
3841
3842 // QUOTE
3843 formatQuote: function()
3844 {
3845 this.bufferSet();
3846
3847 // paragraphy
3848 if (this.opts.linebreaks === false)
3849 {
3850 this.selectionSave();
3851
3852 var blocks = this.getBlocks();
3853
3854 var blockquote = false;
3855 var blocksLen = blocks.length;
3856 if (blocks)
3857 {
3858 var data = '';
3859 var replaced = '';
3860 var replace = false;
3861 var paragraphsOnly = true;
3862
3863 $.each(blocks, function(i,s)
3864 {
3865 if (s.tagName !== 'P') paragraphsOnly = false;
3866 });
3867
3868 $.each(blocks, $.proxy(function(i,s)
3869 {
3870 if (s.tagName === 'BLOCKQUOTE')
3871 {
3872 this.formatBlock('p', s, false);
3873 }
3874 else if (s.tagName === 'P')
3875 {
3876 blockquote = $(s).parent();
3877 // from blockquote
3878 if (blockquote[0].tagName == 'BLOCKQUOTE')
3879 {
3880 var count = $(blockquote).children('p').size();
3881
3882 // one
3883 if (count == 1)
3884 {
3885 $(blockquote).replaceWith(s);
3886 }
3887 // all
3888 else if (count == blocksLen)
3889 {
3890 replace = 'blockquote';
3891 data += this.outerHtml(s);
3892 }
3893 // some
3894 else
3895 {
3896 replace = 'html';
3897 data += this.outerHtml(s);
3898
3899 if (i == 0)
3900 {
3901 $(s).addClass('redactor-replaced').empty();
3902 replaced = this.outerHtml(s);
3903 }
3904 else $(s).remove();
3905 }
3906 }
3907 // to blockquote
3908 else
3909 {
3910 if (paragraphsOnly === false || blocks.length == 1)
3911 {
3912 this.formatBlock('blockquote', s, false);
3913 }
3914 else
3915 {
3916 replace = 'paragraphs';
3917 data += this.outerHtml(s);
3918 }
3919 }
3920
3921 }
3922 else if (s.tagName !== 'LI')
3923 {
3924 this.formatBlock('blockquote', s, false);
3925 }
3926
3927 }, this));
3928
3929 if (replace)
3930 {
3931 if (replace == 'paragraphs')
3932 {
3933 $(blocks[0]).replaceWith('<blockquote>' + data + '</blockquote>');
3934 $(blocks).remove();
3935 }
3936 else if (replace == 'blockquote')
3937 {
3938 $(blockquote).replaceWith(data);
3939 }
3940 else if (replace == 'html')
3941 {
3942 var html = this.$editor.html().replace(replaced, '</blockquote>' + data + '<blockquote>');
3943
3944 this.$editor.html(html);
3945 this.$editor.find('blockquote').each(function()
3946 {
3947 if ($.trim($(this).html()) == '') $(this).remove();
3948 })
3949 }
3950 }
3951 }
3952
3953 this.selectionRestore();
3954 }
3955 // linebreaks
3956 else
3957 {
3958 var block = this.getBlock();
3959 if (block.tagName === 'BLOCKQUOTE')
3960 {
3961 this.selectionSave();
3962
3963 var html = $.trim($(block).html());
3964 var selection = $.trim(this.getSelectionHtml());
3965
3966 html = html.replace(/<span(.*?)id="selection-marker(.*?)<\/span>/gi, '');
3967
3968 if (html == selection)
3969 {
3970 $(block).replaceWith($(block).html() + '<br>');
3971 }
3972 else
3973 {
3974 // replace
3975 this.inlineFormat('tmp');
3976 var tmp = this.$editor.find('tmp');
3977 tmp.empty();
3978
3979 var newhtml = this.$editor.html().replace('<tmp></tmp>', '</blockquote><span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>' + selection + '<blockquote>');
3980
3981 this.$editor.html(newhtml);
3982 tmp.remove();
3983 this.$editor.find('blockquote').each(function()
3984 {
3985 if ($.trim($(this).html()) == '') $(this).remove();
3986 })
3987 }
3988
3989 this.selectionRestore();
3990 this.$editor.find('span#selection-marker-1').attr('id', false);
3991 }
3992 else
3993 {
3994 var wrapper = this.selectionWrap('blockquote');
3995 var html = $(wrapper).html();
3996
3997 var blocksElemsRemove = ['ul', 'ol', 'table', 'tr', 'tbody', 'thead', 'tfoot', 'dl'];
3998 $.each(blocksElemsRemove, function(i,s)
3999 {
4000 html = html.replace(new RegExp('<' + s + '(.*?)>', 'gi'), '');
4001 html = html.replace(new RegExp('</' + s + '>', 'gi'), '');
4002 });
4003
4004 var blocksElems = this.opts.blockLevelElements;
4005 $.each(blocksElems, function(i,s)
4006 {
4007 html = html.replace(new RegExp('<' + s + '(.*?)>', 'gi'), '');
4008 html = html.replace(new RegExp('</' + s + '>', 'gi'), '<br>');
4009 });
4010
4011 $(wrapper).html(html);
4012 this.selectionElement(wrapper);
4013 var next = $(wrapper).next();
4014 if (next.size() != 0 && next[0].tagName === 'BR')
4015 {
4016 next.remove();
4017 }
4018 }
4019 }
4020
4021 this.sync();
4022 },
4023
4024 // BLOCK
4025 blockRemoveAttr: function(attr, value)
4026 {
4027 var nodes = this.getBlocks();
4028 $(nodes).removeAttr(attr);
4029
4030 this.sync();
4031 },
4032 blockSetAttr: function(attr, value)
4033 {
4034 var nodes = this.getBlocks();
4035 $(nodes).attr(attr, value);
4036
4037 this.sync();
4038 },
4039 blockRemoveStyle: function(rule)
4040 {
4041 var nodes = this.getBlocks();
4042 $(nodes).css(rule, '');
4043 this.removeEmptyAttr(nodes, 'style');
4044
4045 this.sync();
4046 },
4047 blockSetStyle: function (rule, value)
4048 {
4049 var nodes = this.getBlocks();
4050 $(nodes).css(rule, value);
4051
4052 this.sync();
4053 },
4054 blockRemoveClass: function(className)
4055 {
4056 var nodes = this.getBlocks();
4057 $(nodes).removeClass(className);
4058 this.removeEmptyAttr(nodes, 'class');
4059
4060 this.sync();
4061 },
4062 blockSetClass: function(className)
4063 {
4064 var nodes = this.getBlocks();
4065 $(nodes).addClass(className);
4066
4067 this.sync();
4068 },
4069
4070 // INLINE
4071 inlineRemoveClass: function(className)
4072 {
4073 this.selectionSave();
4074
4075 this.inlineEachNodes(function(node)
4076 {
4077 $(node).removeClass(className);
4078 this.removeEmptyAttr(node, 'class');
4079 });
4080
4081 this.selectionRestore();
4082 this.sync();
4083 },
4084
4085 inlineSetClass: function(className)
4086 {
4087 var current = this.getCurrent();
4088 if (!$(current).hasClass(className)) this.inlineMethods('addClass', className);
4089 },
4090 inlineRemoveStyle: function (rule)
4091 {
4092 this.selectionSave();
4093
4094 this.inlineEachNodes(function(node)
4095 {
4096 $(node).css(rule, '');
4097 this.removeEmptyAttr(node, 'style');
4098 });
4099
4100 this.selectionRestore();
4101 this.sync();
4102 },
4103 inlineSetStyle: function(rule, value)
4104 {
4105 this.inlineMethods('css', rule, value);
4106 },
4107 inlineRemoveAttr: function (attr)
4108 {
4109 this.selectionSave();
4110
4111 var range = this.getRange(), node = this.getElement(), nodes = this.getNodes();
4112
4113 if (range.collapsed || range.startContainer === range.endContainer && node)
4114 {
4115 nodes = $( node );
4116 }
4117
4118 $(nodes).removeAttr(attr);
4119
4120 this.inlineUnwrapSpan();
4121
4122 this.selectionRestore();
4123 this.sync();
4124 },
4125 inlineSetAttr: function(attr, value)
4126 {
4127 this.inlineMethods('attr', attr, value );
4128 },
4129 inlineMethods: function(type, attr, value)
4130 {
4131 this.bufferSet();
4132 this.selectionSave();
4133
4134 var range = this.getRange()
4135 var el = this.getElement();
4136
4137 if ((range.collapsed || range.startContainer === range.endContainer) && el && !this.nodeTestBlocks(el))
4138 {
4139 console.debug("1");
4140 $(el)[type](attr, value);
4141 }
4142 else
4143 {
4144 console.debug("2");
4145 this.document.execCommand('fontSize', false, 4 );
4146
4147 var fonts = this.$editor.find('font');
4148 $.each(fonts, $.proxy(function(i, s)
4149 {
4150 this.inlineSetMethods(type, s, attr, value);
4151
4152 }, this));
4153
4154 }
4155
4156 this.selectionRestore();
4157 this.sync();
4158 },
4159 inlineSetMethods: function(type, s, attr, value)
4160 {
4161 var parent = $(s).parent(), el;
4162
4163 var selectionHtml = this.getSelectionText();
4164 var parentHtml = $(parent).text();
4165 var selected = selectionHtml == parentHtml;
4166
4167 if (selected && parent && parent[0].tagName === 'INLINE' && parent[0].attributes.length != 0)
4168 {
4169 el = parent;
4170 $(s).replaceWith($(s).html());
4171 }
4172 else
4173 {
4174 el = $('<inline>').append($(s).contents());
4175 $(s).replaceWith(el);
4176 }
4177
4178
4179 $(el)[type](attr, value);
4180
4181 return el;
4182 },
4183 // Sort elements and execute callback
4184 inlineEachNodes: function(callback)
4185 {
4186 var range = this.getRange(),
4187 node = this.getElement(),
4188 nodes = this.getNodes(),
4189 collapsed;
4190
4191 if (range.collapsed || range.startContainer === range.endContainer && node)
4192 {
4193 nodes = $(node);
4194 collapsed = true;
4195 }
4196
4197 $.each(nodes, $.proxy(function(i, node)
4198 {
4199 if (!collapsed && node.tagName !== 'INLINE')
4200 {
4201 var selectionHtml = this.getSelectionText();
4202 var parentHtml = $(node).parent().text();
4203 var selected = selectionHtml == parentHtml;
4204
4205 if (selected && node.parentNode.tagName === 'INLINE' && !$(node.parentNode).hasClass('redactor_editor'))
4206 {
4207 node = node.parentNode;
4208 }
4209 else return;
4210 }
4211 callback.call(this, node);
4212
4213 }, this ) );
4214 },
4215 inlineUnwrapSpan: function()
4216 {
4217 var $spans = this.$editor.find('inline');
4218
4219 $.each($spans, $.proxy(function(i, span)
4220 {
4221 var $span = $(span);
4222
4223 if ($span.attr('class') === undefined && $span.attr('style') === undefined)
4224 {
4225 $span.contents().unwrap();
4226 }
4227
4228 }, this));
4229 },
4230 inlineFormat: function(tag)
4231 {
4232 this.selectionSave();
4233
4234 this.document.execCommand('fontSize', false, 4 );
4235
4236 var fonts = this.$editor.find('font');
4237 var last;
4238 $.each(fonts, function(i, s)
4239 {
4240 var el = $('<' + tag + '/>').append($(s).contents());
4241 $(s).replaceWith(el);
4242 last = el;
4243 });
4244
4245 this.selectionRestore();
4246
4247 this.sync();
4248 },
4249 inlineRemoveFormat: function(tag)
4250 {
4251 this.selectionSave();
4252
4253 var utag = tag.toUpperCase();
4254 var nodes = this.getNodes();
4255 var parent = $(this.getParent()).parent();
4256
4257 $.each(nodes, function(i, s)
4258 {
4259 if (s.tagName === utag) this.inlineRemoveFormatReplace(s);
4260 });
4261
4262 if (parent && parent[0].tagName === utag) this.inlineRemoveFormatReplace(parent);
4263
4264 this.selectionRestore();
4265 this.sync();
4266 },
4267 inlineRemoveFormatReplace: function(el)
4268 {
4269 $(el).replaceWith($(el).contents());
4270 },
4271
4272 // INSERT
4273 insertHtml: function (html, sync)
4274 {
4275 var current = this.getCurrent();
4276 var parent = current.parentNode;
4277
4278 this.focusWithSaveScroll();
4279
4280 this.bufferSet();
4281
4282 var $html = $('<div>').append($.parseHTML(html));
4283 html = $html.html();
4284
4285 html = this.cleanRemoveEmptyTags(html);
4286
4287 // Update value
4288 $html = $('<div>').append($.parseHTML(html));
4289 var currBlock = this.getBlock();
4290
4291 if ($html.contents().length == 1)
4292 {
4293 var htmlTagName = $html.contents()[0].tagName;
4294
4295 // If the inserted and received text tags match
4296 if (htmlTagName != 'P' && htmlTagName == currBlock.tagName || htmlTagName == 'PRE')
4297 {
4298 //html = $html.html();
4299 $html = $('<div>').append(html);
4300 }
4301 }
4302
4303 if (this.opts.linebreaks)
4304 {
4305 html = html.replace(/<p(.*?)>([\w\W]*?)<\/p>/gi, '$2<br>');
4306 }
4307
4308 // add text in a paragraph
4309 if (!this.opts.linebreaks && $html.contents().length == 1 && $html.contents()[0].nodeType == 3
4310 && (this.getRangeSelectedNodes().length > 2 || (!current || current.tagName == 'BODY' && !parent || parent.tagName == 'HTML')))
4311 {
4312 html = '<p>' + html + '</p>';
4313 }
4314
4315 html = this.setSpansVerifiedHtml(html);
4316
4317 if ($html.contents().length > 1 && currBlock
4318 || $html.contents().is('p, :header, ul, ol, li, div, table, td, blockquote, pre, address, section, header, footer, aside, article'))
4319 {
4320 if (this.browser('msie'))
4321 {
4322 if (!this.isIe11())
4323 {
4324 this.document.selection.createRange().pasteHTML(html);
4325 }
4326 else
4327 {
4328 this.execPasteFrag(html);
4329 }
4330 }
4331 else
4332 {
4333 this.document.execCommand('inserthtml', false, html);
4334 }
4335 }
4336 else this.insertHtmlAdvanced(html, false);
4337
4338 if (this.selectall)
4339 {
4340 this.window.setTimeout($.proxy(function()
4341 {
4342 if (!this.opts.linebreaks) this.selectionEnd(this.$editor.contents().last());
4343 else this.focusEnd();
4344
4345 }, this), 1);
4346 }
4347
4348 this.observeStart();
4349
4350 // set no editable
4351 this.setNonEditable();
4352
4353 if (sync !== false) this.sync();
4354 },
4355 insertHtmlAdvanced: function(html, sync)
4356 {
4357 html = this.setSpansVerifiedHtml(html);
4358
4359 var sel = this.getSelection();
4360
4361 if (sel.getRangeAt && sel.rangeCount)
4362 {
4363 var range = sel.getRangeAt(0);
4364 range.deleteContents();
4365
4366 var el = this.document.createElement('div');
4367 el.innerHTML = html;
4368 var frag = this.document.createDocumentFragment(), node, lastNode;
4369 while ((node = el.firstChild))
4370 {
4371 lastNode = frag.appendChild(node);
4372 }
4373
4374 range.insertNode(frag);
4375
4376 if (lastNode)
4377 {
4378 range = range.cloneRange();
4379 range.setStartAfter(lastNode);
4380 range.collapse(true);
4381 sel.removeAllRanges();
4382 sel.addRange(range);
4383 }
4384 }
4385
4386 if (sync !== false)
4387 {
4388 this.sync();
4389 }
4390
4391 },
4392 insertBeforeCursor: function(html)
4393 {
4394 html = this.setSpansVerifiedHtml(html);
4395
4396 var node = $(html);
4397
4398 var space = document.createElement("span");
4399 space.innerHTML = "\u200B";
4400
4401 var range = this.getRange();
4402 range.insertNode(space);
4403 range.insertNode(node[0]);
4404 range.collapse(false);
4405
4406 var sel = this.getSelection();
4407 sel.removeAllRanges();
4408 sel.addRange(range);
4409
4410 this.sync();
4411 },
4412 insertText: function(html)
4413 {
4414 var $html = $($.parseHTML(html));
4415
4416 if ($html.length) html = $html.text();
4417
4418 this.focusWithSaveScroll();
4419
4420 if (this.browser('msie') && !this.isIe11()) this.document.selection.createRange().pasteHTML(html);
4421 else this.document.execCommand('inserthtml', false, html);
4422
4423 this.sync();
4424 },
4425 insertNode: function(node)
4426 {
4427 node = node[0] || node;
4428
4429 if (node.tagName == 'SPAN')
4430 {
4431 var replacementTag = 'inline';
4432
4433 var outer = node.outerHTML;
4434
4435 // Replace opening tag
4436 var regex = new RegExp('<' + node.tagName, 'i');
4437 var newTag = outer.replace(regex, '<' + replacementTag);
4438
4439 // Replace closing tag
4440 regex = new RegExp('</' + node.tagName, 'i');
4441 newTag = newTag.replace(regex, '</' + replacementTag);
4442 node = $(newTag)[0];
4443 }
4444
4445 var sel = this.getSelection();
4446 if (sel.getRangeAt && sel.rangeCount)
4447 {
4448 // with delete contents
4449 range = sel.getRangeAt(0);
4450 range.deleteContents();
4451 range.insertNode(node);
4452 range.setEndAfter(node);
4453 range.setStartAfter(node);
4454 sel.removeAllRanges();
4455 sel.addRange(range);
4456 }
4457 },
4458 insertNodeToCaretPositionFromPoint: function(e, node)
4459 {
4460 var range;
4461 var x = e.clientX, y = e.clientY;
4462 if (this.document.caretPositionFromPoint)
4463 {
4464 var pos = this.document.caretPositionFromPoint(x, y);
4465 range = this.getRange();
4466 range.setStart(pos.offsetNode, pos.offset);
4467 range.collapse(true);
4468 range.insertNode(node);
4469 }
4470 else if (this.document.caretRangeFromPoint)
4471 {
4472 range = this.document.caretRangeFromPoint(x, y);
4473 range.insertNode(node);
4474 }
4475 else if (typeof document.body.createTextRange != "undefined")
4476 {
4477 range = this.document.body.createTextRange();
4478 range.moveToPoint(x, y);
4479 var endRange = range.duplicate();
4480 endRange.moveToPoint(x, y);
4481 range.setEndPoint("EndToEnd", endRange);
4482 range.select();
4483 }
4484
4485 },
4486 insertAfterLastElement: function(element, parent)
4487 {
4488 if (typeof(parent) != 'undefined') element = parent;
4489
4490 if (this.isEndOfElement())
4491 {
4492 if (this.opts.linebreaks)
4493 {
4494 var contents = $('<div>').append($.trim(this.$editor.html())).contents();
4495 var last = contents.last()[0];
4496 if (last.tagName == 'SPAN' && last.innerHTML == '')
4497 {
4498 last = contents.prev()[0];
4499 }
4500
4501 if (this.outerHtml(last) != this.outerHtml(element))
4502 {
4503 return false;
4504 }
4505 }
4506 else
4507 {
4508 if (this.$editor.contents().last()[0] !== element)
4509 {
4510 return false;
4511 }
4512 }
4513
4514 this.insertingAfterLastElement(element);
4515 }
4516 },
4517 insertingAfterLastElement: function(element)
4518 {
4519 this.bufferSet();
4520
4521 if (this.opts.linebreaks === false)
4522 {
4523 var node = $(this.opts.emptyHtml);
4524 $(element).after(node);
4525 this.selectionStart(node);
4526 }
4527 else
4528 {
4529 var node = $('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>', this.document)[0];
4530 $(element).after(node);
4531 $(node).after(this.opts.invisibleSpace);
4532 this.selectionRestore();
4533 this.$editor.find('span#selection-marker-1').removeAttr('id');
4534 }
4535 },
4536 insertLineBreak: function(twice)
4537 {
4538 this.selectionSave();
4539
4540 var br = '<br>';
4541 if (twice == true)
4542 {
4543 br = '<br><br>';
4544 }
4545
4546 if (this.browser('mozilla'))
4547 {
4548 var span = $('<span>').html(this.opts.invisibleSpace);
4549 this.$editor.find('#selection-marker-1').before(br).before(span).before(this.opts.invisibleSpace);
4550
4551 this.setCaretAfter(span[0]);
4552 span.remove();
4553
4554 this.selectionRemoveMarkers();
4555 }
4556 else
4557 {
4558 var parent = this.getParent();
4559 if (parent && parent.tagName === 'A')
4560 {
4561 var offset = this.getCaretOffset(parent);
4562
4563 var text = $.trim($(parent).text()).replace(/\n\r\n/g, '');
4564 var len = text.length;
4565
4566 if (offset == len)
4567 {
4568 this.selectionRemoveMarkers();
4569
4570 var node = $('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>', this.document)[0];
4571 $(parent).after(node);
4572 $(node).before(br + (this.browser('webkit') ? this.opts.invisibleSpace : ''));
4573 this.selectionRestore();
4574
4575 return true;
4576 }
4577
4578 }
4579
4580 this.$editor.find('#selection-marker-1').before(br + (this.browser('webkit') ? this.opts.invisibleSpace : ''));
4581 this.selectionRestore();
4582 }
4583 },
4584 insertDoubleLineBreak: function()
4585 {
4586 this.insertLineBreak(true);
4587 },
4588 replaceLineBreak: function(element)
4589 {
4590 var node = $('<br>' + this.opts.invisibleSpace);
4591 $(element).replaceWith(node);
4592 this.selectionStart(node);
4593 },
4594
4595 // PASTE
4596 pasteClean: function(html)
4597 {
4598 html = this.callback('pasteBefore', false, html);
4599
4600 // ie10 fix paste links
4601 if (this.browser('msie'))
4602 {
4603 var tmp = $.trim(html);
4604 if (tmp.search(/^<a(.*?)>(.*?)<\/a>$/i) == 0)
4605 {
4606 html = html.replace(/^<a(.*?)>(.*?)<\/a>$/i, "$2");
4607 }
4608 }
4609
4610 if (this.opts.pastePlainText)
4611 {
4612 var tmp = this.document.createElement('div');
4613
4614 html = html.replace(/<br>|<\/H[1-6]>|<\/p>|<\/div>/gi, '\n');
4615
4616 tmp.innerHTML = html;
4617 html = tmp.textContent || tmp.innerText;
4618
4619 html = $.trim(html);
4620 html = html.replace('\n', '<br>');
4621 html = this.cleanParagraphy(html);
4622
4623 this.pasteInsert(html);
4624 return false;
4625 }
4626
4627 // clean up table
4628 var tablePaste = false;
4629 if (this.currentOrParentIs('TD'))
4630 {
4631 tablePaste = true;
4632 var blocksElems = this.opts.blockLevelElements;
4633 blocksElems.push('tr');
4634 blocksElems.push('table');
4635 $.each(blocksElems, function(i,s)
4636 {
4637 html = html.replace(new RegExp('<' + s + '(.*?)>', 'gi'), '');
4638 html = html.replace(new RegExp('</' + s + '>', 'gi'), '<br>');
4639 });
4640 }
4641
4642 // clean up pre
4643 if (this.currentOrParentIs('PRE'))
4644 {
4645 html = this.pastePre(html);
4646 this.pasteInsert(html);
4647 return true;
4648 }
4649
4650 // ms words shapes
4651 html = html.replace(/<img(.*?)v:shapes=(.*?)>/gi, '');
4652
4653 // ms word list
4654 html = html.replace(/<p(.*?)class="MsoListParagraphCxSpFirst"([\w\W]*?)<\/p>/gi, '<ul><li$2</li>');
4655 html = html.replace(/<p(.*?)class="MsoListParagraphCxSpMiddle"([\w\W]*?)<\/p>/gi, '<li$2</li>');
4656 html = html.replace(/<p(.*?)class="MsoListParagraphCxSpLast"([\w\W]*?)<\/p>/gi, '<li$2</li></ul>');
4657 // one line
4658 html = html.replace(/<p(.*?)class="MsoListParagraph"([\w\W]*?)<\/p>/gi, '<ul><li$2</li></ul>');
4659 // remove ms word's bullet
4660 html = html.replace(/·/g, '');
4661
4662 // remove comments and php tags
4663 html = html.replace(/<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi, '');
4664
4665 // remove nbsp
4666 if (this.opts.cleanSpaces === true)
4667 {
4668 html = html.replace(/(&nbsp;){2,}/gi, '&nbsp;');
4669 html = html.replace(/&nbsp;/gi, ' ');
4670 }
4671
4672 // remove google docs marker
4673 html = html.replace(/<b\sid="internal-source-marker(.*?)">([\w\W]*?)<\/b>/gi, "$2");
4674 html = html.replace(/<b(.*?)id="docs-internal-guid(.*?)">([\w\W]*?)<\/b>/gi, "$3");
4675
4676 // strip tags
4677 html = this.cleanStripTags(html);
4678
4679 // prevert
4680 html = html.replace(/<td>\u200b*<\/td>/gi, '[td]');
4681 html = html.replace(/<td>&nbsp;<\/td>/gi, '[td]');
4682 html = html.replace(/<td><br><\/td>/gi, '[td]');
4683 html = html.replace(/<td(.*?)colspan="(.*?)"(.*?)>([\w\W]*?)<\/td>/gi, '[td colspan="$2"]$4[/td]');
4684 html = html.replace(/<td(.*?)rowspan="(.*?)"(.*?)>([\w\W]*?)<\/td>/gi, '[td rowspan="$2"]$4[/td]');
4685 html = html.replace(/<a(.*?)href="(.*?)"(.*?)>([\w\W]*?)<\/a>/gi, '[a href="$2"]$4[/a]');
4686 html = html.replace(/<iframe(.*?)>([\w\W]*?)<\/iframe>/gi, '[iframe$1]$2[/iframe]');
4687 html = html.replace(/<video(.*?)>([\w\W]*?)<\/video>/gi, '[video$1]$2[/video]');
4688 html = html.replace(/<audio(.*?)>([\w\W]*?)<\/audio>/gi, '[audio$1]$2[/audio]');
4689 html = html.replace(/<embed(.*?)>([\w\W]*?)<\/embed>/gi, '[embed$1]$2[/embed]');
4690 html = html.replace(/<object(.*?)>([\w\W]*?)<\/object>/gi, '[object$1]$2[/object]');
4691 html = html.replace(/<param(.*?)>/gi, '[param$1]');
4692 html = html.replace(/<img(.*?)>/gi, '[img$1]');
4693
4694 // remove classes
4695 html = html.replace(/ class="(.*?)"/gi, '');
4696
4697 // remove all attributes
4698 html = html.replace(/<(\w+)([\w\W]*?)>/gi, '<$1>');
4699
4700 // remove empty
4701 if (this.opts.linebreaks)
4702 {
4703 // prevent double linebreaks when an empty line in RTF has bold or underlined formatting associated with it
4704 html = html.replace(/<strong><\/strong>/gi, '');
4705 html = html.replace(/<u><\/u>/gi, '');
4706
4707 if (this.opts.cleanFontTag)
4708 {
4709 html = html.replace(/<font(.*?)>([\w\W]*?)<\/font>/gi, '$2');
4710 }
4711
4712 html = html.replace(/<[^\/>][^>]*>(\s*|\t*|\n*|&nbsp;|<br>)<\/[^>]+>/gi, '<br>');
4713 }
4714 else
4715 {
4716 html = html.replace(/<[^\/>][^>]*>(\s*|\t*|\n*|&nbsp;|<br>)<\/[^>]+>/gi, '');
4717 }
4718
4719 html = html.replace(/<div>\s*?\t*?\n*?(<ul>|<ol>|<p>)/gi, '$1');
4720
4721 // revert
4722 html = html.replace(/\[td colspan="(.*?)"\]([\w\W]*?)\[\/td\]/gi, '<td colspan="$1">$2</td>');
4723 html = html.replace(/\[td rowspan="(.*?)"\]([\w\W]*?)\[\/td\]/gi, '<td rowspan="$1">$2</td>');
4724 html = html.replace(/\[td\]/gi, '<td>&nbsp;</td>');
4725 html = html.replace(/\[a href="(.*?)"\]([\w\W]*?)\[\/a\]/gi, '<a href="$1">$2</a>');
4726 html = html.replace(/\[iframe(.*?)\]([\w\W]*?)\[\/iframe\]/gi, '<iframe$1>$2</iframe>');
4727 html = html.replace(/\[video(.*?)\]([\w\W]*?)\[\/video\]/gi, '<video$1>$2</video>');
4728 html = html.replace(/\[audio(.*?)\]([\w\W]*?)\[\/audio\]/gi, '<audio$1>$2</audio>');
4729 html = html.replace(/\[embed(.*?)\]([\w\W]*?)\[\/embed\]/gi, '<embed$1>$2</embed>');
4730 html = html.replace(/\[object(.*?)\]([\w\W]*?)\[\/object\]/gi, '<object$1>$2</object>');
4731 html = html.replace(/\[param(.*?)\]/gi, '<param$1>');
4732 html = html.replace(/\[img(.*?)\]/gi, '<img$1>');
4733
4734 // convert div to p
4735 if (this.opts.convertDivs)
4736 {
4737 html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '<p>$2</p>');
4738 html = html.replace(/<\/div><p>/gi, '<p>');
4739 html = html.replace(/<\/p><\/div>/gi, '</p>');
4740 html = html.replace(/<p><\/p>/gi, '<br />');
4741 }
4742 else
4743 {
4744 html = html.replace(/<div><\/div>/gi, '<br />');
4745 }
4746
4747 if (this.currentOrParentIs('LI'))
4748 {
4749 html = html.replace(/<p>([\w\W]*?)<\/p>/gi, '$1<br>');
4750 }
4751 else if (tablePaste === false)
4752 {
4753 html = this.cleanParagraphy(html);
4754 }
4755
4756 // remove span
4757 html = html.replace(/<span(.*?)>([\w\W]*?)<\/span>/gi, '$2');
4758
4759 // remove empty
4760 html = html.replace(/<img>/gi, '');
4761 html = html.replace(/<[^\/>][^>][^img|param|source|td][^<]*>(\s*|\t*|\n*| |<br>)<\/[^>]+>/gi, '');
4762
4763 html = html.replace(/\n{3,}/gi, '\n');
4764
4765 // remove dirty p
4766 html = html.replace(/<p><p>/gi, '<p>');
4767 html = html.replace(/<\/p><\/p>/gi, '</p>');
4768
4769 html = html.replace(/<li>(\s*|\t*|\n*)<p>/gi, '<li>');
4770 html = html.replace(/<\/p>(\s*|\t*|\n*)<\/li>/gi, '</li>');
4771
4772 if (this.opts.linebreaks === true)
4773 {
4774 html = html.replace(/<p(.*?)>([\w\W]*?)<\/p>/gi, '$2<br>');
4775 }
4776
4777 // remove empty finally
4778 html = html.replace(/<[^\/>][^>][^img|param|source|td][^<]*>(\s*|\t*|\n*| |<br>)<\/[^>]+>/gi, '');
4779
4780 // remove safari local images
4781 html = html.replace(/<img src="webkit-fake-url\:\/\/(.*?)"(.*?)>/gi, '');
4782
4783 // remove p in td
4784 html = html.replace(/<td(.*?)>(\s*|\t*|\n*)<p>([\w\W]*?)<\/p>(\s*|\t*|\n*)<\/td>/gi, '<td$1>$3</td>');
4785
4786 // remove divs
4787 if (this.opts.convertDivs)
4788 {
4789 html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '$2');
4790 html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '$2');
4791 }
4792
4793 // FF specific
4794 this.pasteClipboardMozilla = false;
4795 if (this.browser('mozilla'))
4796 {
4797 if (this.opts.clipboardUpload)
4798 {
4799 var matches = html.match(/<img src="data:image(.*?)"(.*?)>/gi);
4800 if (matches !== null)
4801 {
4802 this.pasteClipboardMozilla = matches;
4803 for (k in matches)
4804 {
4805 var img = matches[k].replace('<img', '<img data-mozilla-paste-image="' + k + '" ');
4806 html = html.replace(matches[k], img);
4807 }
4808 }
4809 }
4810
4811 // FF fix
4812 while (/<br>$/gi.test(html))
4813 {
4814 html = html.replace(/<br>$/gi, '');
4815 }
4816 }
4817
4818 // bullets again
4819 html = html.replace(/<p>•([\w\W]*?)<\/p>/gi, '<li>$1</li>');
4820
4821 // ie inserts a blank font tags when pasting
4822 if (this.browser('msie'))
4823 {
4824 while (/<font>([\w\W]*?)<\/font>/gi.test(html))
4825 {
4826 html = html.replace(/<font>([\w\W]*?)<\/font>/gi, '$1');
4827 }
4828 }
4829
4830 // remove table paragraphs
4831 if (tablePaste === false)
4832 {
4833 html = html.replace(/<td(.*?)>([\w\W]*?)<p(.*?)>([\w\W]*?)<\/td>/gi, '<td$1>$2$4</td>');
4834 html = html.replace(/<td(.*?)>([\w\W]*?)<\/p>([\w\W]*?)<\/td>/gi, '<td$1>$2$3</td>');
4835 html = html.replace(/<td(.*?)>([\w\W]*?)<p(.*?)>([\w\W]*?)<\/td>/gi, '<td$1>$2$4</td>');
4836 html = html.replace(/<td(.*?)>([\w\W]*?)<\/p>([\w\W]*?)<\/td>/gi, '<td$1>$2$3</td>');
4837 }
4838
4839 // ms word break lines
4840 html = html.replace(/\n/g, ' ');
4841
4842 // ms word lists break lines
4843 html = html.replace(/<p>\n?<li>/gi, '<li>');
4844
4845 this.pasteInsert(html);
4846
4847 },
4848 pastePre: function(s)
4849 {
4850 s = s.replace(/<br>|<\/H[1-6]>|<\/p>|<\/div>/gi, '\n');
4851
4852 var tmp = this.document.createElement('div');
4853 tmp.innerHTML = s;
4854 return this.cleanEncodeEntities(tmp.textContent || tmp.innerText);
4855 },
4856 pasteInsert: function(html)
4857 {
4858 html = this.callback('pasteAfter', false, html);
4859
4860 if (this.selectall)
4861 {
4862 this.$editor.html(html);
4863 this.selectionRemove();
4864 this.focusEnd();
4865 this.sync();
4866 }
4867 else
4868 {
4869 this.insertHtml(html);
4870 }
4871
4872 this.selectall = false;
4873
4874 setTimeout($.proxy(function()
4875 {
4876 this.rtePaste = false;
4877
4878 // FF specific
4879 if (this.browser('mozilla'))
4880 {
4881 this.$editor.find('p:empty').remove()
4882 }
4883 if (this.pasteClipboardMozilla !== false)
4884 {
4885 this.pasteClipboardUploadMozilla();
4886 }
4887
4888 }, this), 100);
4889
4890 if (this.opts.autoresize && this.fullscreen !== true)
4891 {
4892 $(this.document.body).scrollTop(this.saveScroll);
4893 }
4894 else
4895 {
4896 this.$editor.scrollTop(this.saveScroll);
4897 }
4898 },
4899 pasteClipboardAppendFields: function(postData)
4900 {
4901 // append hidden fields
4902 if (this.opts.uploadFields !== false && typeof this.opts.uploadFields === 'object')
4903 {
4904 $.each(this.opts.uploadFields, $.proxy(function(k, v)
4905 {
4906 if (v != null && v.toString().indexOf('#') === 0) v = $(v).val();
4907 postData[k] = v;
4908
4909 }, this));
4910 }
4911
4912 return postData;
4913 },
4914 pasteClipboardUploadMozilla: function()
4915 {
4916 var imgs = this.$editor.find('img[data-mozilla-paste-image]');
4917 $.each(imgs, $.proxy(function(i,s)
4918 {
4919 var $s = $(s);
4920 var arr = s.src.split(",");
4921 var postData = {
4922 'contentType': arr[0].split(";")[0].split(":")[1],
4923 'data': arr[1] // raw base64
4924 };
4925
4926 // append hidden fields
4927 postData = this.pasteClipboardAppendFields(postData);
4928
4929 $.post(this.opts.clipboardUploadUrl, postData,
4930 $.proxy(function(data)
4931 {
4932 var json = (typeof data === 'string' ? $.parseJSON(data) : data);
4933 $s.attr('src', json.filelink);
4934 $s.removeAttr('data-mozilla-paste-image');
4935
4936 this.sync();
4937
4938 // upload callback
4939 this.callback('imageUpload', $s, json);
4940
4941 }, this));
4942
4943 }, this));
4944 },
4945 pasteClipboardUpload: function(e)
4946 {
4947 var result = e.target.result;
4948 var arr = result.split(",");
4949 var postData = {
4950 'contentType': arr[0].split(";")[0].split(":")[1],
4951 'data': arr[1] // raw base64
4952 };
4953
4954
4955 if (this.opts.clipboardUpload)
4956 {
4957 // append hidden fields
4958 postData = this.pasteClipboardAppendFields(postData);
4959
4960 $.post(this.opts.clipboardUploadUrl, postData,
4961 $.proxy(function(data)
4962 {
4963 var json = (typeof data === 'string' ? $.parseJSON(data) : data);
4964
4965 var html = '<img src="' + json.filelink + '" id="clipboard-image-marker" />';
4966 this.execCommand('inserthtml', html, false);
4967
4968 var image = $(this.$editor.find('img#clipboard-image-marker'));
4969
4970 if (image.length) image.removeAttr('id');
4971 else image = false;
4972
4973 this.sync();
4974
4975 // upload callback
4976 if (image)
4977 {
4978 this.callback('imageUpload', image, json);
4979 }
4980
4981
4982 }, this));
4983 }
4984 else
4985 {
4986 this.insertHtml('<img src="' + result + '" />');
4987 }
4988 },
4989
4990 // BUFFER
4991 bufferSet: function(html, selectionSave)
4992 {
4993 if (html !== undefined || html === false) this.opts.buffer.push(html);
4994 else
4995 {
4996 if (selectionSave !== false)
4997 {
4998 this.selectionSave();
4999 }
5000
5001 this.opts.buffer.push(this.$editor.html());
5002 this.selectionRemoveMarkers('buffer');
5003 }
5004 },
5005 bufferUndo: function()
5006 {
5007 if (this.opts.buffer.length === 0)
5008 {
5009 this.focusWithSaveScroll();
5010 return;
5011 }
5012
5013 // rebuffer
5014 this.selectionSave();
5015 this.opts.rebuffer.push(this.$editor.html());
5016 this.selectionRestore(false, true);
5017
5018 this.$editor.html(this.opts.buffer.pop());
5019
5020 this.selectionRestore();
5021 setTimeout($.proxy(this.observeStart, this), 100);
5022 },
5023 bufferRedo: function()
5024 {
5025 if (this.opts.rebuffer.length === 0)
5026 {
5027 this.focusWithSaveScroll();
5028 return false;
5029 }
5030
5031 // buffer
5032 this.selectionSave();
5033 this.opts.buffer.push(this.$editor.html());
5034 this.selectionRestore(false, true);
5035
5036 this.$editor.html(this.opts.rebuffer.pop());
5037 this.selectionRestore(true);
5038 setTimeout($.proxy(this.observeStart, this), 4);
5039 },
5040
5041 // OBSERVE
5042 observeStart: function()
5043 {
5044 this.observeImages();
5045
5046 if (this.opts.observeLinks) this.observeLinks();
5047 },
5048 observeLinks: function()
5049 {
5050 this.$editor.find('a').on('click', $.proxy(this.linkObserver, this));
5051 this.$editor.on('click.redactor', $.proxy(function(e)
5052 {
5053 this.linkObserverTooltipClose(e);
5054
5055 }, this));
5056 $(document).on('click.redactor', $.proxy(function(e)
5057 {
5058 this.linkObserverTooltipClose(e);
5059
5060 }, this));
5061 },
5062 observeImages: function()
5063 {
5064 if (this.opts.observeImages === false) return false;
5065
5066 this.$editor.find('img').each($.proxy(function(i, elem)
5067 {
5068 if (this.browser('msie')) $(elem).attr('unselectable', 'on');
5069 this.imageResize(elem);
5070
5071 }, this));
5072 },
5073 linkObserver: function(e)
5074 {
5075 var $link = $(e.target);
5076
5077 if ($link.size() == 0 || $link[0].tagName !== 'A') return;
5078
5079 var pos = $link.offset();
5080 if (this.opts.iframe)
5081 {
5082 var posFrame = this.$frame.offset();
5083 pos.top = posFrame.top + (pos.top - $(this.document).scrollTop());
5084 pos.left += posFrame.left;
5085 }
5086
5087 var tooltip = $('<span class="redactor-link-tooltip"></span>');
5088
5089 var href = $link.attr('href');
5090 if (href === undefined)
5091 {
5092 href = '';
5093 }
5094
5095 if (href.length > 24) href = href.substring(0, 24) + '...';
5096
5097 var aLink = $('<a href="' + $link.attr('href') + '" target="_blank">' + href + '</a>').on('click', $.proxy(function(e)
5098 {
5099 this.linkObserverTooltipClose(false);
5100 }, this));
5101
5102 var aEdit = $('<a href="#">' + this.opts.curLang.edit + '</a>').on('click', $.proxy(function(e)
5103 {
5104 e.preventDefault();
5105 this.linkShow();
5106 this.linkObserverTooltipClose(false);
5107
5108 }, this));
5109
5110 var aUnlink = $('<a href="#">' + this.opts.curLang.unlink + '</a>').on('click', $.proxy(function(e)
5111 {
5112 e.preventDefault();
5113 this.execCommand('unlink');
5114 this.linkObserverTooltipClose(false);
5115
5116 }, this));
5117
5118
5119 tooltip.append(aLink);
5120 tooltip.append(' | ');
5121 tooltip.append(aEdit);
5122 tooltip.append(' | ');
5123 tooltip.append(aUnlink);
5124 tooltip.css({
5125 top: (pos.top + 20) + 'px',
5126 left: pos.left + 'px'
5127 });
5128
5129 $('.redactor-link-tooltip').remove();
5130 $('body').append(tooltip);
5131 },
5132 linkObserverTooltipClose: function(e)
5133 {
5134 if (e !== false && e.target.tagName == 'A') return false;
5135 $('.redactor-link-tooltip').remove();
5136 },
5137
5138 // SELECTION
5139 getSelection: function()
5140 {
5141 if (!this.opts.rangy) return this.document.getSelection();
5142 else // rangy
5143 {
5144 if (!this.opts.iframe) return rangy.getSelection();
5145 else return rangy.getSelection(this.$frame[0]);
5146 }
5147 },
5148 getRange: function()
5149 {
5150 if (!this.opts.rangy)
5151 {
5152 if (this.document.getSelection)
5153 {
5154 var sel = this.getSelection();
5155 if (sel.getRangeAt && sel.rangeCount) return sel.getRangeAt(0);
5156 }
5157
5158 return this.document.createRange();
5159 }
5160 else // rangy
5161 {
5162 if (!this.opts.iframe) return rangy.createRange();
5163 else return rangy.createRange(this.iframeDoc());
5164 }
5165 },
5166 selectionElement: function(node)
5167 {
5168 this.setCaret(node);
5169 },
5170 selectionStart: function(node)
5171 {
5172 this.selectionSet(node[0] || node, 0, null, 0);
5173 },
5174 selectionEnd: function(node)
5175 {
5176 this.selectionSet(node[0] || node, 1, null, 1);
5177 },
5178 selectionSet: function(orgn, orgo, focn, foco)
5179 {
5180 if (focn == null) focn = orgn;
5181 if (foco == null) foco = orgo;
5182
5183 var sel = this.getSelection();
5184 if (!sel) return;
5185
5186 if (orgn.tagName == 'P' && orgn.innerHTML == '')
5187 {
5188 orgn.innerHTML = this.opts.invisibleSpace;
5189 }
5190
5191 if (orgn.tagName == 'BR' && this.opts.linebreaks === false)
5192 {
5193 var par = $(this.opts.emptyHtml)[0];
5194 $(orgn).replaceWith(par);
5195 orgn = par;
5196 focn = orgn;
5197 }
5198
5199 var range = this.getRange();
5200 range.setStart(orgn, orgo);
5201 range.setEnd(focn, foco );
5202
5203 try {
5204 sel.removeAllRanges();
5205 } catch (e) {}
5206
5207 sel.addRange(range);
5208 },
5209 selectionWrap: function(tag)
5210 {
5211 tag = tag.toLowerCase();
5212
5213 var block = this.getBlock();
5214 if (block)
5215 {
5216 var wrapper = this.formatChangeTag(block, tag);
5217 this.sync();
5218 return wrapper;
5219 }
5220
5221 var sel = this.getSelection();
5222 var range = sel.getRangeAt(0);
5223 var wrapper = document.createElement(tag);
5224 wrapper.appendChild(range.extractContents());
5225 range.insertNode(wrapper);
5226
5227 this.selectionElement(wrapper);
5228
5229 return wrapper;
5230 },
5231 selectionAll: function()
5232 {
5233 var range = this.getRange();
5234 range.selectNodeContents(this.$editor[0]);
5235
5236 var sel = this.getSelection();
5237 sel.removeAllRanges();
5238 sel.addRange(range);
5239 },
5240 selectionRemove: function()
5241 {
5242 this.getSelection().removeAllRanges();
5243 },
5244 getCaretOffset: function (element)
5245 {
5246 var caretOffset = 0;
5247
5248 var range = this.getRange();
5249 var preCaretRange = range.cloneRange();
5250 preCaretRange.selectNodeContents(element);
5251 preCaretRange.setEnd(range.endContainer, range.endOffset);
5252 caretOffset = $.trim(preCaretRange.toString()).length;
5253
5254 return caretOffset;
5255 },
5256 getCaretOffsetRange: function()
5257 {
5258 return new Range(this.getSelection().getRangeAt(0));
5259 },
5260 setCaret: function (el, start, end)
5261 {
5262 if (typeof end === 'undefined') end = start;
5263 el = el[0] || el;
5264
5265 var range = this.getRange();
5266 range.selectNodeContents(el);
5267
5268 var textNodes = this.getTextNodesIn(el);
5269 var foundStart = false;
5270 var charCount = 0, endCharCount;
5271
5272 if (textNodes.length == 1 && start)
5273 {
5274 range.setStart(textNodes[0], start);
5275 range.setEnd(textNodes[0], end);
5276 }
5277 else
5278 {
5279 for (var i = 0, textNode; textNode = textNodes[i++];)
5280 {
5281 endCharCount = charCount + textNode.length;
5282 if (!foundStart && start >= charCount && (start < endCharCount || (start == endCharCount && i < textNodes.length)))
5283 {
5284 range.setStart(textNode, start - charCount);
5285 foundStart = true;
5286 }
5287
5288 if (foundStart && end <= endCharCount)
5289 {
5290 range.setEnd( textNode, end - charCount );
5291 break;
5292 }
5293
5294 charCount = endCharCount;
5295 }
5296 }
5297
5298 var sel = this.getSelection();
5299 sel.removeAllRanges();
5300 sel.addRange( range );
5301 },
5302 setCaretAfter: function(node)
5303 {
5304 this.$editor.focus();
5305
5306 node = node[0] || node;
5307
5308 var range = this.document.createRange()
5309
5310 var start = 1;
5311 var end = -1;
5312
5313 range.setStart(node, start)
5314 range.setEnd(node, end + 2)
5315
5316
5317 var selection = this.window.getSelection()
5318 var cursorRange = this.document.createRange()
5319
5320 var emptyElement = this.document.createTextNode('\u200B')
5321 $(node).after(emptyElement)
5322
5323 cursorRange.setStartAfter(emptyElement)
5324
5325 selection.removeAllRanges()
5326 selection.addRange(cursorRange)
5327 $(emptyElement).remove();
5328 },
5329 getTextNodesIn: function (node)
5330 {
5331 var textNodes = [];
5332
5333 if (node.nodeType == 3) textNodes.push(node);
5334 else
5335 {
5336 var children = node.childNodes;
5337 for (var i = 0, len = children.length; i < len; ++i)
5338 {
5339 textNodes.push.apply(textNodes, this.getTextNodesIn(children[i]));
5340 }
5341 }
5342
5343 return textNodes;
5344 },
5345
5346 // GET ELEMENTS
5347 getCurrent: function()
5348 {
5349 var el = false;
5350 var sel = this.getSelection();
5351
5352 if (sel && sel.rangeCount > 0)
5353 {
5354 el = sel.getRangeAt(0).startContainer;
5355 //el = sel.getRangeAt(0).commonAncestorContainer;
5356 }
5357
5358 return this.isParentRedactor(el);
5359 },
5360 getParent: function(elem)
5361 {
5362 elem = elem || this.getCurrent();
5363 if (elem) return this.isParentRedactor( $( elem ).parent()[0] );
5364 else return false;
5365 },
5366 getBlock: function(node)
5367 {
5368 if (typeof node === 'undefined') node = this.getCurrent();
5369
5370 while (node)
5371 {
5372 if (this.nodeTestBlocks(node))
5373 {
5374 if ($(node).hasClass('redactor_editor')) return false;
5375 return node;
5376 }
5377
5378 node = node.parentNode;
5379 }
5380
5381 return false;
5382 },
5383 getBlocks: function(nodes)
5384 {
5385 var newnodes = [];
5386 if (typeof nodes == 'undefined')
5387 {
5388 var range = this.getRange();
5389 if (range && range.collapsed === true) return [this.getBlock()];
5390 var nodes = this.getNodes(range);
5391 }
5392
5393 $.each(nodes, $.proxy(function(i,node)
5394 {
5395 if (this.opts.iframe === false && $(node).parents('div.redactor_editor').size() == 0) return false;
5396 if (this.nodeTestBlocks(node)) newnodes.push(node);
5397
5398 }, this));
5399
5400 if (newnodes.length === 0) newnodes = [this.getBlock()];
5401
5402 return newnodes;
5403 },
5404 nodeTestBlocks: function(node)
5405 {
5406 return node.nodeType == 1 && this.rTestBlock.test(node.nodeName);
5407 },
5408 tagTestBlock: function(tag)
5409 {
5410 return this.rTestBlock.test(tag);
5411 },
5412 getNodes: function(range, tag)
5413 {
5414 if (typeof range == 'undefined' || range == false) var range = this.getRange();
5415 if (range && range.collapsed === true)
5416 {
5417 if (typeof tag === 'undefined' && this.tagTestBlock(tag))
5418 {
5419 var block = this.getBlock();
5420 if (block.tagName == tag) return [block];
5421 else return [];
5422 }
5423 else
5424 {
5425 return [this.getCurrent()];
5426 }
5427 }
5428
5429 var nodes = [], finalnodes = [];
5430
5431 var sel = this.document.getSelection();
5432 if (!sel.isCollapsed) nodes = this.getRangeSelectedNodes(sel.getRangeAt(0));
5433
5434 $.each(nodes, $.proxy(function(i,node)
5435 {
5436 if (this.opts.iframe === false && $(node).parents('div.redactor_editor').size() == 0) return false;
5437
5438 if (typeof tag === 'undefined')
5439 {
5440 if ($.trim(node.textContent) != '')
5441 {
5442 finalnodes.push(node);
5443 }
5444 }
5445 else if (node.tagName == tag)
5446 {
5447 finalnodes.push(node);
5448 }
5449
5450 }, this));
5451
5452 if (finalnodes.length == 0)
5453 {
5454 if (typeof tag === 'undefined' && this.tagTestBlock(tag))
5455 {
5456 var block = this.getBlock();
5457 if (block.tagName == tag) return finalnodes.push(block);
5458 else return [];
5459 }
5460 else
5461 {
5462 finalnodes.push(this.getCurrent());
5463 }
5464 }
5465
5466 // last element filtering
5467 var last = finalnodes[finalnodes.length-1];
5468 if (this.nodeTestBlocks(last))
5469 {
5470 finalnodes = finalnodes.slice(0, -1);
5471 }
5472
5473 return finalnodes;
5474 },
5475 getElement: function(node)
5476 {
5477 if (!node) node = this.getCurrent();
5478 while (node)
5479 {
5480 if (node.nodeType == 1)
5481 {
5482 if ($(node).hasClass('redactor_editor')) return false;
5483 return node;
5484 }
5485
5486 node = node.parentNode;
5487 }
5488
5489 return false;
5490 },
5491 getRangeSelectedNodes: function(range)
5492 {
5493 range = range || this.getRange();
5494 var node = range.startContainer;
5495 var endNode = range.endContainer;
5496
5497 if (node == endNode) return [node];
5498
5499 var rangeNodes = [];
5500 while (node && node != endNode)
5501 {
5502 rangeNodes.push(node = this.nextNode(node));
5503 }
5504
5505 node = range.startContainer;
5506 while (node && node != range.commonAncestorContainer)
5507 {
5508 rangeNodes.unshift(node);
5509 node = node.parentNode;
5510 }
5511
5512 return rangeNodes;
5513 },
5514 nextNode: function(node)
5515 {
5516 if (node.hasChildNodes()) return node.firstChild;
5517 else
5518 {
5519 while (node && !node.nextSibling)
5520 {
5521 node = node.parentNode;
5522 }
5523
5524 if (!node) return null;
5525 return node.nextSibling;
5526 }
5527 },
5528
5529 // GET SELECTION HTML OR TEXT
5530 getSelectionText: function()
5531 {
5532 return this.getSelection().toString();
5533 },
5534 getSelectionHtml: function()
5535 {
5536 var html = '';
5537
5538 var sel = this.getSelection();
5539 if (sel.rangeCount)
5540 {
5541 var container = this.document.createElement( "div" );
5542 var len = sel.rangeCount;
5543 for (var i = 0; i < len; ++i)
5544 {
5545 container.appendChild(sel.getRangeAt(i).cloneContents());
5546 }
5547
5548 html = container.innerHTML;
5549 }
5550
5551 return this.syncClean(html);
5552 },
5553
5554 // SAVE & RESTORE
5555 selectionSave: function()
5556 {
5557 if (!this.isFocused()) this.focusWithSaveScroll();
5558
5559 if (!this.opts.rangy)
5560 {
5561 this.selectionCreateMarker(this.getRange());
5562 }
5563 // rangy
5564 else
5565 {
5566 this.savedSel = rangy.saveSelection();
5567 }
5568 },
5569 selectionCreateMarker: function(range, remove)
5570 {
5571 if (!range) return;
5572
5573 var node1 = $('<span id="selection-marker-1" class="redactor-selection-marker">' + this.opts.invisibleSpace + '</span>', this.document)[0];
5574 var node2 = $('<span id="selection-marker-2" class="redactor-selection-marker">' + this.opts.invisibleSpace + '</span>', this.document)[0];
5575
5576 if (range.collapsed === true)
5577 {
5578 this.selectionSetMarker(range, node1, true);
5579 }
5580 else
5581 {
5582 this.selectionSetMarker(range, node1, true);
5583 this.selectionSetMarker(range, node2, false);
5584 }
5585
5586 this.savedSel = this.$editor.html();
5587
5588 this.selectionRestore(false, false);
5589 },
5590 selectionSetMarker: function(range, node, type)
5591 {
5592 var boundaryRange = range.cloneRange();
5593
5594 boundaryRange.collapse(type);
5595
5596 boundaryRange.insertNode(node);
5597 boundaryRange.detach();
5598 },
5599 selectionRestore: function(replace, remove)
5600 {
5601 if (!this.opts.rangy)
5602 {
5603 if (replace === true && this.savedSel)
5604 {
5605 this.$editor.html(this.savedSel);
5606 }
5607
5608 var node1 = this.$editor.find('span#selection-marker-1');
5609 var node2 = this.$editor.find('span#selection-marker-2');
5610
5611 if (this.browser('mozilla'))
5612 {
5613 this.$editor.focus();
5614 }
5615 else if (!this.isFocused())
5616 {
5617 this.focusWithSaveScroll();
5618 }
5619
5620 if (node1.length != 0 && node2.length != 0)
5621 {
5622 this.selectionSet(node1[0], 0, node2[0], 0);
5623 }
5624 else if (node1.length != 0)
5625 {
5626 this.selectionSet(node1[0], 0, null, 0);
5627 }
5628
5629 if (remove !== false)
5630 {
5631 this.selectionRemoveMarkers();
5632 this.savedSel = false;
5633 }
5634 }
5635 // rangy
5636 else
5637 {
5638 rangy.restoreSelection(this.savedSel);
5639 }
5640 },
5641 selectionRemoveMarkers: function(type)
5642 {
5643 if (!this.opts.rangy)
5644 {
5645 $.each(this.$editor.find('span.redactor-selection-marker'), function()
5646 {
5647 var html = $.trim($(this).html().replace(/[^\u0000-\u1C7F]/g, ''));
5648 if (html == '')
5649 {
5650 $(this).remove();
5651 }
5652 else
5653 {
5654 $(this).removeAttr('class').removeAttr('id');
5655 }
5656 });
5657 }
5658 // rangy
5659 else
5660 {
5661 rangy.removeMarkers(this.savedSel);
5662 }
5663 },
5664
5665 // TABLE
5666 tableShow: function()
5667 {
5668 this.selectionSave();
5669
5670 this.modalInit(this.opts.curLang.table, this.opts.modal_table, 300, $.proxy(function()
5671 {
5672 $('#redactor_insert_table_btn').click($.proxy(this.tableInsert, this));
5673
5674 setTimeout(function()
5675 {
5676 $('#redactor_table_rows').focus();
5677
5678 }, 200);
5679
5680 }, this));
5681 },
5682 tableInsert: function()
5683 {
5684 this.bufferSet(false, false);
5685
5686 var rows = $('#redactor_table_rows').val(),
5687 columns = $('#redactor_table_columns').val(),
5688 $table_box = $('<div></div>'),
5689 tableId = Math.floor(Math.random() * 99999),
5690 $table = $('<table id="table' + tableId + '"><tbody></tbody></table>'),
5691 i, $row, z, $column;
5692
5693 for (i = 0; i < rows; i++)
5694 {
5695 $row = $('<tr></tr>');
5696
5697 for (z = 0; z < columns; z++)
5698 {
5699 $column = $('<td>' + this.opts.invisibleSpace + '</td>');
5700
5701 // set the focus to the first td
5702 if (i === 0 && z === 0)
5703 {
5704 $column.append('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>');
5705 }
5706
5707 $($row).append($column);
5708 }
5709
5710 $table.append($row);
5711 }
5712
5713 $table_box.append($table);
5714 var html = $table_box.html();
5715
5716 this.modalClose();
5717 this.selectionRestore();
5718
5719 var current = this.getBlock() || this.getCurrent();
5720
5721 if (current && current.tagName != 'BODY')
5722 {
5723 if (current.tagName == 'LI')
5724 {
5725 var current = $(current).closest('ul, ol');
5726 }
5727
5728 $(current).after(html)
5729 }
5730 else
5731 {
5732 this.insertHtmlAdvanced(html, false);
5733 }
5734
5735 this.selectionRestore();
5736
5737 var table = this.$editor.find('#table' + tableId);
5738 this.buttonActiveObserver();
5739
5740 table.find('span#selection-marker-1, inline#selection-marker-1').remove();
5741 table.removeAttr('id');
5742
5743 this.sync();
5744 },
5745 tableDeleteTable: function()
5746 {
5747 var $table = $(this.getParent()).closest('table');
5748 if (!this.isParentRedactor($table)) return false;
5749 if ($table.size() == 0) return false;
5750
5751 this.bufferSet();
5752
5753 $table.remove();
5754 this.sync();
5755 },
5756 tableDeleteRow: function()
5757 {
5758 var parent = this.getParent();
5759 var $table = $(parent).closest('table');
5760
5761
5762 if (!this.isParentRedactor($table)) return false;
5763 if ($table.size() == 0) return false;
5764
5765 this.bufferSet();
5766
5767 var $current_tr = $(parent).closest('tr');
5768 var $focus_tr = $current_tr.prev().length ? $current_tr.prev() : $current_tr.next();
5769 if ($focus_tr.length)
5770 {
5771 var $focus_td = $focus_tr.children('td' ).first();
5772 if ($focus_td.length)
5773 {
5774 $focus_td.prepend('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>');
5775 }
5776 }
5777
5778 $current_tr.remove();
5779 this.selectionRestore();
5780 this.sync();
5781 },
5782 tableDeleteColumn: function()
5783 {
5784 var parent = this.getParent();
5785 var $table = $(parent).closest('table');
5786
5787 if (!this.isParentRedactor($table)) return false;
5788 if ($table.size() == 0) return false;
5789
5790 this.bufferSet();
5791
5792 var $current_td = $(parent).closest('td');
5793 if (!($current_td.is('td')))
5794 {
5795 $current_td = $current_td.closest('td');
5796 }
5797
5798 var index = $current_td.get(0).cellIndex;
5799
5800 // Set the focus correctly
5801 $table.find('tr').each($.proxy(function(i, elem)
5802 {
5803 var focusIndex = index - 1 < 0 ? index + 1 : index - 1;
5804 if (i === 0)
5805 {
5806 $(elem).find('td').eq(focusIndex).prepend('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>');
5807 }
5808
5809 $(elem).find('td').eq(index).remove();
5810
5811 }, this));
5812
5813 this.selectionRestore();
5814 this.sync();
5815 },
5816 tableAddHead: function()
5817 {
5818 var $table = $(this.getParent()).closest('table');
5819 if (!this.isParentRedactor($table)) return false;
5820 if ($table.size() == 0) return false;
5821
5822 this.bufferSet();
5823
5824 if ($table.find('thead').size() !== 0) this.tableDeleteHead();
5825 else
5826 {
5827 var tr = $table.find('tr').first().clone();
5828 tr.find('td').html(this.opts.invisibleSpace);
5829 $thead = $('<thead></thead>');
5830 $thead.append(tr);
5831 $table.prepend($thead);
5832
5833 this.sync();
5834 }
5835 },
5836 tableDeleteHead: function()
5837 {
5838 var $table = $(this.getParent()).closest('table');
5839 if (!this.isParentRedactor($table)) return false;
5840 var $thead = $table.find('thead');
5841
5842 if ($thead.size() == 0) return false;
5843
5844 this.bufferSet();
5845
5846 $thead.remove();
5847 this.sync();
5848 },
5849 tableAddRowAbove: function()
5850 {
5851 this.tableAddRow('before');
5852 },
5853 tableAddRowBelow: function()
5854 {
5855 this.tableAddRow('after');
5856 },
5857 tableAddColumnLeft: function()
5858 {
5859 this.tableAddColumn('before');
5860 },
5861 tableAddColumnRight: function()
5862 {
5863 this.tableAddColumn('after');
5864 },
5865 tableAddRow: function(type)
5866 {
5867 var $table = $(this.getParent()).closest('table');
5868 if (!this.isParentRedactor($table)) return false;
5869 if ($table.size() == 0) return false;
5870
5871 this.bufferSet();
5872
5873 var $current_tr = $(this.getParent()).closest('tr');
5874 var new_tr = $current_tr.clone();
5875 new_tr.find('td').html(this.opts.invisibleSpace);
5876
5877 if (type === 'after') $current_tr.after(new_tr);
5878 else $current_tr.before(new_tr);
5879
5880 this.sync();
5881 },
5882 tableAddColumn: function (type)
5883 {
5884 var parent = this.getParent();
5885 var $table = $(parent).closest('table');
5886
5887 if (!this.isParentRedactor($table)) return false;
5888 if ($table.size() == 0) return false;
5889
5890 this.bufferSet();
5891
5892 var index = 0;
5893
5894 var current = this.getCurrent();
5895 var $current_tr = $(current).closest('tr');
5896 var $current_td = $(current).closest('td');
5897
5898 $current_tr.find('td').each($.proxy(function(i, elem)
5899 {
5900 if ($(elem)[0] === $current_td[0]) index = i;
5901
5902 }, this));
5903
5904 $table.find('tr').each($.proxy(function(i, elem)
5905 {
5906 var $current = $(elem).find('td').eq(index);
5907
5908 var td = $current.clone();
5909 td.html(this.opts.invisibleSpace);
5910
5911 type === 'after' ? $current.after(td) : $current.before(td);
5912
5913 }, this));
5914
5915 this.sync();
5916 },
5917
5918 // VIDEO
5919 videoShow: function()
5920 {
5921 this.selectionSave();
5922
5923 this.modalInit(this.opts.curLang.video, this.opts.modal_video, 600, $.proxy(function()
5924 {
5925 $('#redactor_insert_video_btn').click($.proxy(this.videoInsert, this));
5926
5927 setTimeout(function()
5928 {
5929 $('#redactor_insert_video_area').focus();
5930
5931 }, 200);
5932
5933 }, this));
5934 },
5935 videoInsert: function ()
5936 {
5937 var data = $('#redactor_insert_video_area').val();
5938 data = this.cleanStripTags(data);
5939
5940 this.selectionRestore();
5941
5942 var current = this.getBlock() || this.getCurrent();
5943
5944 if (current) $(current).after(data)
5945 else this.insertHtmlAdvanced(data, false);
5946
5947 this.sync();
5948 this.modalClose();
5949 },
5950
5951
5952 // LINK
5953 linkShow: function()
5954 {
5955 this.selectionSave();
5956
5957 var callback = $.proxy(function()
5958 {
5959 this.insert_link_node = false;
5960
5961 var sel = this.getSelection();
5962 var url = '', text = '', target = '';
5963
5964 var elem = this.getParent();
5965 var par = $(elem).parent().get(0);
5966 if (par && par.tagName === 'A')
5967 {
5968 elem = par;
5969 }
5970
5971 if (elem && elem.tagName === 'A')
5972 {
5973 url = elem.href;
5974 text = $(elem).text();
5975 target = elem.target;
5976
5977 this.insert_link_node = elem;
5978 }
5979 else text = sel.toString();
5980
5981 $('#redactor_link_url_text').val(text);
5982
5983 var thref = self.location.href.replace(/\/$/i, '');
5984 url = url.replace(thref, '');
5985 url = url.replace(/^\/#/, '#');
5986 url = url.replace('mailto:', '');
5987
5988 // remove host from href
5989 if (this.opts.linkProtocol === false)
5990 {
5991 var re = new RegExp('^(http|ftp|https)://' + self.location.host, 'i');
5992 url = url.replace(re, '');
5993 }
5994
5995 // set url
5996 $('#redactor_link_url').val(url);
5997
5998 if (target === '_blank')
5999 {
6000 $('#redactor_link_blank').prop('checked', true);
6001 }
6002
6003 this.linkInsertPressed = false;
6004 $('#redactor_insert_link_btn').click($.proxy(this.linkProcess, this));
6005
6006 setTimeout(function()
6007 {
6008 $('#redactor_link_url').focus();
6009
6010 }, 200);
6011
6012 }, this);
6013
6014 this.modalInit(this.opts.curLang.link, this.opts.modal_link, 460, callback);
6015
6016 },
6017 linkProcess: function()
6018 {
6019 if (this.linkInsertPressed)
6020 {
6021 return;
6022 }
6023
6024 this.linkInsertPressed = true;
6025 var target = '', targetBlank = '';
6026
6027 var link = $('#redactor_link_url').val();
6028 var text = $('#redactor_link_url_text').val();
6029
6030 // mailto
6031 if (link.search('@') != -1)
6032 {
6033 link = 'mailto:' + link;
6034 }
6035 // url, not anchor
6036 else if (link.search('#') != 0)
6037 {
6038 if ($('#redactor_link_blank').prop('checked'))
6039 {
6040 target = ' target="_blank"';
6041 targetBlank = '_blank';
6042 }
6043
6044 // test url (add protocol)
6045 var pattern = '((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}';
6046 var re = new RegExp('^(http|ftp|https)://' + pattern, 'i');
6047 var re2 = new RegExp('^' + pattern, 'i');
6048
6049 if (link.search(re) == -1 && link.search(re2) == 0 && this.opts.linkProtocol)
6050 {
6051 link = this.opts.linkProtocol + link;
6052 }
6053 }
6054
6055 text = text.replace(/<|>/g, '');
6056 var extra = '&nbsp;';
6057 if (this.browser('mozilla'))
6058 {
6059 extra = '&nbsp;';
6060 }
6061
6062 this.linkInsert('<a href="' + link + '"' + target + '>' + text + '</a>' + extra, $.trim(text), link, targetBlank);
6063
6064 },
6065 linkInsert: function (a, text, link, target)
6066 {
6067 this.selectionRestore();
6068
6069 if (text !== '')
6070 {
6071 if (this.insert_link_node)
6072 {
6073 this.bufferSet();
6074
6075 $(this.insert_link_node).text(text).attr('href', link);
6076
6077 if (target !== '')
6078 {
6079 $(this.insert_link_node).attr('target', target);
6080 }
6081 else
6082 {
6083 $(this.insert_link_node).removeAttr('target');
6084 }
6085 }
6086 else
6087 {
6088 var $a = $(a).addClass('redactor-added-link');
6089 this.exec('inserthtml', this.outerHtml($a), false);
6090
6091 var link = this.$editor.find('a.redactor-added-link');
6092
6093 link.removeAttr('style').removeClass('redactor-added-link').each(function()
6094 {
6095 if (this.className == '') $(this).removeAttr('class');
6096 });
6097
6098 }
6099
6100 this.sync();
6101 }
6102
6103 // link tooltip
6104 setTimeout($.proxy(function()
6105 {
6106 if (this.opts.observeLinks) this.observeLinks();
6107
6108 }, this), 5);
6109
6110 this.modalClose();
6111 },
6112
6113 // FILE
6114 fileShow: function ()
6115 {
6116
6117 this.selectionSave();
6118
6119 var callback = $.proxy(function()
6120 {
6121 var sel = this.getSelection();
6122
6123 var text = '';
6124 if (this.oldIE()) text = sel.text;
6125 else text = sel.toString();
6126
6127 $('#redactor_filename').val(text);
6128
6129 // dragupload
6130 if (!this.isMobile() && !this.isIPad())
6131 {
6132 this.draguploadInit('#redactor_file', {
6133 url: this.opts.fileUpload,
6134 uploadFields: this.opts.uploadFields,
6135 success: $.proxy(this.fileCallback, this),
6136 error: $.proxy( function(obj, json)
6137 {
6138 this.callback('fileUploadError', json);
6139
6140 }, this),
6141 uploadParam: this.opts.fileUploadParam
6142 });
6143 }
6144
6145 this.uploadInit('redactor_file', {
6146 auto: true,
6147 url: this.opts.fileUpload,
6148 success: $.proxy(this.fileCallback, this),
6149 error: $.proxy(function(obj, json)
6150 {
6151 this.callback('fileUploadError', json);
6152
6153 }, this)
6154 });
6155
6156 }, this);
6157
6158 this.modalInit(this.opts.curLang.file, this.opts.modal_file, 500, callback);
6159 },
6160 fileCallback: function(json)
6161 {
6162
6163 this.selectionRestore();
6164
6165 if (json !== false)
6166 {
6167
6168 var text = $('#redactor_filename').val();
6169 if (text === '') text = json.filename;
6170
6171 var link = '<a href="' + json.filelink + '" id="filelink-marker">' + text + '</a>';
6172
6173 // chrome fix
6174 if (this.browser('webkit') && !!this.window.chrome)
6175 {
6176 link = link + '&nbsp;';
6177 }
6178
6179 this.execCommand('inserthtml', link, false);
6180
6181 var linkmarker = $(this.$editor.find('a#filelink-marker'));
6182 if (linkmarker.size() != 0) linkmarker.removeAttr('id');
6183 else linkmarker = false;
6184
6185 this.sync();
6186
6187 // file upload callback
6188 this.callback('fileUpload', linkmarker, json);
6189 }
6190
6191 this.modalClose();
6192 },
6193
6194 // IMAGE
6195 imageShow: function()
6196 {
6197
6198 this.selectionSave();
6199
6200 var callback = $.proxy(function()
6201 {
6202 // json
6203 if (this.opts.imageGetJson)
6204 {
6205 $.getJSON(this.opts.imageGetJson, $.proxy(function(data)
6206 {
6207 var folders = {}, count = 0;
6208
6209 // folders
6210 $.each(data, $.proxy(function(key, val)
6211 {
6212 if (typeof val.folder !== 'undefined')
6213 {
6214 count++;
6215 folders[val.folder] = count;
6216 }
6217
6218 }, this));
6219
6220 var folderclass = false;
6221 $.each(data, $.proxy(function(key, val)
6222 {
6223 // title
6224 var thumbtitle = '';
6225 if (typeof val.title !== 'undefined') thumbtitle = val.title;
6226
6227 var folderkey = 0;
6228 if (!$.isEmptyObject(folders) && typeof val.folder !== 'undefined')
6229 {
6230 folderkey = folders[val.folder];
6231 if (folderclass === false) folderclass = '.redactorfolder' + folderkey;
6232 }
6233
6234 var img = $('<img src="' + val.thumb + '" class="redactorfolder redactorfolder' + folderkey + '" rel="' + val.image + '" title="' + thumbtitle + '" />');
6235 $('#redactor_image_box').append(img);
6236 $(img).click($.proxy(this.imageThumbClick, this));
6237
6238 }, this));
6239
6240 // folders
6241 if (!$.isEmptyObject(folders))
6242 {
6243 $('.redactorfolder').hide();
6244 $(folderclass).show();
6245
6246 var onchangeFunc = function(e)
6247 {
6248 $('.redactorfolder').hide();
6249 $('.redactorfolder' + $(e.target).val()).show();
6250 };
6251
6252 var select = $('<select id="redactor_image_box_select">');
6253 $.each( folders, function(k, v)
6254 {
6255 select.append( $('<option value="' + v + '">' + k + '</option>'));
6256 });
6257
6258 $('#redactor_image_box').before(select);
6259 select.change(onchangeFunc);
6260 }
6261 }, this));
6262
6263 }
6264 else
6265 {
6266 $('#redactor-modal-tab-2').remove();
6267 }
6268
6269 if (this.opts.imageUpload || this.opts.s3)
6270 {
6271 // dragupload
6272 if (!this.isMobile() && !this.isIPad() && this.opts.s3 === false)
6273 {
6274 if ($('#redactor_file' ).length)
6275 {
6276 this.draguploadInit('#redactor_file', {
6277 url: this.opts.imageUpload,
6278 uploadFields: this.opts.uploadFields,
6279 success: $.proxy(this.imageCallback, this),
6280 error: $.proxy(function(obj, json)
6281 {
6282 this.callback('imageUploadError', json);
6283
6284 }, this),
6285 uploadParam: this.opts.imageUploadParam
6286 });
6287 }
6288 }
6289
6290 if (this.opts.s3 === false)
6291 {
6292 // ajax upload
6293 this.uploadInit('redactor_file', {
6294 auto: true,
6295 url: this.opts.imageUpload,
6296 success: $.proxy(this.imageCallback, this),
6297 error: $.proxy(function(obj, json)
6298 {
6299 this.callback('imageUploadError', json);
6300
6301 }, this)
6302 });
6303 }
6304 // s3 upload
6305 else
6306 {
6307 $('#redactor_file').on('change.redactor', $.proxy(this.s3handleFileSelect, this));
6308 }
6309
6310 }
6311 else
6312 {
6313 $('.redactor_tab').hide();
6314 if (!this.opts.imageGetJson)
6315 {
6316 $('#redactor_tabs').remove();
6317 $('#redactor_tab3').show();
6318 }
6319 else
6320 {
6321 $('#redactor-modal-tab-1').remove();
6322 $('#redactor-modal-tab-2').addClass('redactor_tabs_act');
6323 $('#redactor_tab2').show();
6324 }
6325 }
6326
6327 if (!this.opts.imageTabLink && (this.opts.imageUpload || this.opts.imageGetJson))
6328 {
6329 $('#redactor-tab-control-3').hide();
6330 }
6331
6332 $('#redactor_upload_btn').click($.proxy(this.imageCallbackLink, this));
6333
6334 if (!this.opts.imageUpload && !this.opts.imageGetJson)
6335 {
6336 setTimeout(function()
6337 {
6338 $('#redactor_file_link').focus();
6339
6340 }, 200);
6341 }
6342
6343 }, this);
6344
6345 this.modalInit(this.opts.curLang.image, this.opts.modal_image, 610, callback);
6346
6347 },
6348 imageEdit: function(image)
6349 {
6350 var $el = image;
6351 var parent = $el.parent().parent();
6352
6353 var callback = $.proxy(function()
6354 {
6355 $('#redactor_file_alt').val($el.attr('alt'));
6356 $('#redactor_image_edit_src').attr('href', $el.attr('src'));
6357
6358 if ($el.css('display') == 'block' && $el.css('float') == 'none')
6359 {
6360 $('#redactor_form_image_align').val('center');
6361 }
6362 else
6363 {
6364 $('#redactor_form_image_align').val($el.css('float'));
6365 }
6366
6367 if ($(parent).get(0).tagName === 'A')
6368 {
6369 $('#redactor_file_link').val($(parent).attr('href'));
6370
6371 if ($(parent).attr('target') == '_blank')
6372 {
6373 $('#redactor_link_blank').prop('checked', true);
6374 }
6375 }
6376
6377 $('#redactor_image_delete_btn').click($.proxy(function()
6378 {
6379 this.imageRemove($el);
6380
6381 }, this));
6382
6383 $('#redactorSaveBtn').click($.proxy(function()
6384 {
6385 this.imageSave($el);
6386
6387 }, this));
6388
6389 }, this);
6390
6391 this.modalInit(this.opts.curLang.edit, this.opts.modal_image_edit, 380, callback);
6392
6393 },
6394 imageRemove: function(el)
6395 {
6396 var parentLink = $(el).parent().parent();
6397 var parent = $(el).parent();
6398 var parentEl = false;
6399
6400 if (parentLink.length && parentLink[0].tagName === 'A')
6401 {
6402 parentEl = true;
6403 $(parentLink).remove();
6404 }
6405 else if (parent.length && parent[0].tagName === 'A')
6406 {
6407 parentEl = true;
6408 $(parent).remove();
6409 }
6410 else
6411 {
6412 $(el).remove();
6413 }
6414
6415 if (parent.length && parent[0].tagName === 'P')
6416 {
6417 this.focusWithSaveScroll();
6418
6419 if (parentEl === false) this.selectionStart(parent);
6420 }
6421
6422 // delete callback
6423 this.callback('imageDelete', el);
6424
6425 this.modalClose();
6426 this.sync();
6427 },
6428 imageSave: function(el)
6429 {
6430 var $el = $(el);
6431 var parent = $el.parent();
6432
6433 $el.attr('alt', $('#redactor_file_alt').val());
6434
6435 var floating = $('#redactor_form_image_align').val();
6436 var margin = '';
6437
6438 this.imageResizeHide(false);
6439
6440 if (floating === 'left')
6441 {
6442 margin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0';
6443 $el.css({ 'float': 'left', 'margin': margin });
6444 }
6445 else if (floating === 'right')
6446 {
6447 margin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + '';
6448 $el.css({ 'float': 'right', 'margin': margin });
6449 }
6450 else if (floating === 'center')
6451 {
6452 $el.css({ 'float': '', 'display': 'block', 'margin': 'auto' });
6453 }
6454 else
6455 {
6456 $el.css({ 'float': '', 'display': '', 'margin': '' });
6457 }
6458
6459 // as link
6460 var link = $.trim($('#redactor_file_link').val());
6461 if (link !== '')
6462 {
6463 var target = false;
6464 if ($('#redactor_link_blank').prop('checked'))
6465 {
6466 target = true;
6467 }
6468
6469 if (parent.get(0).tagName !== 'A')
6470 {
6471 var a = $('<a href="' + link + '">' + this.outerHtml(el) + '</a>');
6472
6473 if (target)
6474 {
6475 a.attr('target', '_blank');
6476 }
6477
6478 $el.replaceWith(a);
6479 }
6480 else
6481 {
6482 parent.attr('href', link);
6483 if (target)
6484 {
6485 parent.attr('target', '_blank');
6486 }
6487 else
6488 {
6489 parent.removeAttr('target');
6490 }
6491 }
6492 }
6493 else
6494 {
6495 if (parent.get(0).tagName === 'A')
6496 {
6497 parent.replaceWith(this.outerHtml(el));
6498 }
6499 }
6500
6501 this.modalClose();
6502 this.observeImages();
6503 this.sync();
6504
6505 },
6506 imageResizeHide: function(e)
6507 {
6508 if (e !== false && $(e.target).parent().size() != 0 && $(e.target).parent()[0].id === 'redactor-image-box')
6509 {
6510 return false;
6511 }
6512
6513 var imageBox = this.$editor.find('#redactor-image-box');
6514 if (imageBox.size() == 0)
6515 {
6516 return false;
6517 }
6518
6519 this.$editor.find('#redactor-image-editter, #redactor-image-resizer').remove();
6520
6521 imageBox.find('img').css({
6522 marginTop: imageBox[0].style.marginTop,
6523 marginBottom: imageBox[0].style.marginBottom,
6524 marginLeft: imageBox[0].style.marginLeft,
6525 marginRight: imageBox[0].style.marginRight
6526 });
6527
6528 imageBox.css('margin', '');
6529
6530
6531 imageBox.find('img').css('opacity', '');
6532 imageBox.replaceWith(function()
6533 {
6534 return $(this).contents();
6535 });
6536
6537 $(document).off('click.redactor-image-resize-hide');
6538 this.$editor.off('click.redactor-image-resize-hide');
6539 this.$editor.off('keydown.redactor-image-delete');
6540
6541 this.sync()
6542
6543 },
6544 imageResize: function(image)
6545 {
6546 var $image = $(image);
6547
6548 $image.on('mousedown', $.proxy(function()
6549 {
6550 this.imageResizeHide(false);
6551 }, this));
6552
6553 $image.on('dragstart', $.proxy(function()
6554 {
6555 this.$editor.on('drop.redactor-image-inside-drop', $.proxy(function()
6556 {
6557 setTimeout($.proxy(function()
6558 {
6559 this.observeImages();
6560 this.$editor.off('drop.redactor-image-inside-drop');
6561 this.sync();
6562
6563 }, this), 1);
6564
6565 },this));
6566 }, this));
6567
6568 $image.on('click', $.proxy(function(e)
6569 {
6570 if (this.$editor.find('#redactor-image-box').size() != 0)
6571 {
6572 return false;
6573 }
6574
6575 var clicked = false,
6576 start_x,
6577 start_y,
6578 ratio = $image.width() / $image.height(),
6579 min_w = 20,
6580 min_h = 10;
6581
6582 var imageResizer = this.imageResizeControls($image);
6583
6584 // resize
6585 var isResizing = false;
6586 imageResizer.on('mousedown', function(e)
6587 {
6588 isResizing = true;
6589 e.preventDefault();
6590
6591 ratio = $image.width() / $image.height();
6592
6593 start_x = Math.round(e.pageX - $image.eq(0).offset().left);
6594 start_y = Math.round(e.pageY - $image.eq(0).offset().top);
6595
6596 });
6597
6598 $(this.document.body).on('mousemove', $.proxy(function(e)
6599 {
6600 if (isResizing)
6601 {
6602 var mouse_x = Math.round(e.pageX - $image.eq(0).offset().left) - start_x;
6603 var mouse_y = Math.round(e.pageY - $image.eq(0).offset().top) - start_y;
6604
6605 var div_h = $image.height();
6606
6607 var new_h = parseInt(div_h, 10) + mouse_y;
6608 var new_w = Math.round(new_h * ratio);
6609
6610 if (new_w > min_w)
6611 {
6612 $image.width(new_w);
6613
6614 if (new_w < 100)
6615 {
6616 this.imageEditter.css({
6617 marginTop: '-7px',
6618 marginLeft: '-13px',
6619 fontSize: '9px',
6620 padding: '3px 5px'
6621 });
6622 }
6623 else
6624 {
6625 this.imageEditter.css({
6626 marginTop: '-11px',
6627 marginLeft: '-18px',
6628 fontSize: '11px',
6629 padding: '7px 10px'
6630 });
6631 }
6632 }
6633
6634 start_x = Math.round(e.pageX - $image.eq(0).offset().left);
6635 start_y = Math.round(e.pageY - $image.eq(0).offset().top);
6636
6637 this.sync()
6638 }
6639 }, this)).on('mouseup', function()
6640 {
6641 isResizing = false;
6642 });
6643
6644
6645 this.$editor.on('keydown.redactor-image-delete', $.proxy(function(e)
6646 {
6647 var key = e.which;
6648
6649 if (this.keyCode.BACKSPACE == key || this.keyCode.DELETE == key)
6650 {
6651 this.bufferSet(false, false);
6652 this.imageResizeHide(false);
6653 this.imageRemove($image);
6654 }
6655
6656 }, this));
6657
6658 $(document).on('click.redactor-image-resize-hide', $.proxy(this.imageResizeHide, this));
6659 this.$editor.on('click.redactor-image-resize-hide', $.proxy(this.imageResizeHide, this));
6660
6661
6662 }, this));
6663 },
6664 imageResizeControls: function($image)
6665 {
6666 var imageBox = $('<span id="redactor-image-box" data-redactor="verified">');
6667 imageBox.css({
6668 position: 'relative',
6669 display: 'inline-block',
6670 lineHeight: 0,
6671 outline: '1px dashed rgba(0, 0, 0, .6)',
6672 'float': $image.css('float')
6673 });
6674 imageBox.attr('contenteditable', false);
6675
6676 if ($image[0].style.margin != 'auto')
6677 {
6678 imageBox.css({
6679 marginTop: $image[0].style.marginTop,
6680 marginBottom: $image[0].style.marginBottom,
6681 marginLeft: $image[0].style.marginLeft,
6682 marginRight: $image[0].style.marginRight
6683 });
6684
6685 $image.css('margin', '');
6686 }
6687 else
6688 {
6689 imageBox.css({ 'display': 'block', 'margin': 'auto' });
6690 }
6691
6692 $image.css('opacity', .5).after(imageBox);
6693
6694 // editter
6695 this.imageEditter = $('<span id="redactor-image-editter" data-redactor="verified">' + this.opts.curLang.edit + '</span>');
6696 this.imageEditter.css({
6697 position: 'absolute',
6698 zIndex: 5,
6699 top: '50%',
6700 left: '50%',
6701 marginTop: '-11px',
6702 marginLeft: '-18px',
6703 lineHeight: 1,
6704 backgroundColor: '#000',
6705 color: '#fff',
6706 fontSize: '11px',
6707 padding: '7px 10px',
6708 cursor: 'pointer'
6709 });
6710 this.imageEditter.attr('contenteditable', false);
6711 this.imageEditter.on('click', $.proxy(function()
6712 {
6713 this.imageEdit($image);
6714 }, this));
6715 imageBox.append(this.imageEditter);
6716
6717 // resizer
6718 var imageResizer = $('<span id="redactor-image-resizer" data-redactor="verified"></span>');
6719 imageResizer.css({
6720 position: 'absolute',
6721 zIndex: 2,
6722 lineHeight: 1,
6723 cursor: 'nw-resize',
6724 bottom: '-4px',
6725 right: '-5px',
6726 border: '1px solid #fff',
6727 backgroundColor: '#000',
6728 width: '8px',
6729 height: '8px'
6730 });
6731 imageResizer.attr('contenteditable', false);
6732 imageBox.append(imageResizer);
6733
6734 imageBox.append($image);
6735
6736 return imageResizer;
6737 },
6738 imageThumbClick: function(e)
6739 {
6740 var img = '<img id="image-marker" src="' + $(e.target).attr('rel') + '" alt="' + $(e.target).attr('title') + '" />';
6741
6742 var parent = this.getParent();
6743 if (this.opts.paragraphy && $(parent).closest('li').size() == 0) img = '<p>' + img + '</p>';
6744
6745 this.imageInsert(img, true);
6746 },
6747 imageCallbackLink: function()
6748 {
6749 var val = $('#redactor_file_link').val();
6750
6751 if (val !== '')
6752 {
6753 var data = '<img id="image-marker" src="' + val + '" />';
6754 if (this.opts.linebreaks === false) data = '<p>' + data + '</p>';
6755
6756 this.imageInsert(data, true);
6757
6758 }
6759 else this.modalClose();
6760 },
6761 imageCallback: function(data)
6762 {
6763 this.imageInsert(data);
6764 },
6765 imageInsert: function(json, link)
6766 {
6767 this.selectionRestore();
6768
6769 if (json !== false)
6770 {
6771 var html = '';
6772 if (link !== true)
6773 {
6774 html = '<img id="image-marker" src="' + json.filelink + '" />';
6775
6776 var parent = this.getParent();
6777 if (this.opts.paragraphy && $(parent).closest('li').size() == 0)
6778 {
6779 html = '<p>' + html + '</p>';
6780 }
6781 }
6782 else
6783 {
6784 html = json;
6785 }
6786
6787 this.execCommand('inserthtml', html, false);
6788
6789 var image = $(this.$editor.find('img#image-marker'));
6790
6791 if (image.length) image.removeAttr('id');
6792 else image = false;
6793
6794 this.sync();
6795
6796 // upload image callback
6797 link !== true && this.callback('imageUpload', image, json);
6798 }
6799
6800 this.modalClose();
6801 this.observeImages();
6802 },
6803
6804 // MODAL
6805 modalTemplatesInit: function()
6806 {
6807 $.extend( this.opts,
6808 {
6809 modal_file: String()
6810 + '<section id="redactor-modal-file-insert">'
6811 + '<div id="redactor-progress" class="redactor-progress-inline" style="display: none;"><span></span></div>'
6812 + '<form id="redactorUploadFileForm" method="post" action="" enctype="multipart/form-data">'
6813 + '<label>' + this.opts.curLang.filename + '</label>'
6814 + '<input type="text" id="redactor_filename" class="redactor_input" />'
6815 + '<div style="margin-top: 7px;">'
6816 + '<input type="file" id="redactor_file" name="' + this.opts.fileUploadParam + '" />'
6817 + '</div>'
6818 + '</form>'
6819 + '</section>',
6820
6821 modal_image_edit: String()
6822 + '<section id="redactor-modal-image-edit">'
6823 + '<label>' + this.opts.curLang.title + '</label>'
6824 + '<input type="text" id="redactor_file_alt" class="redactor_input" />'
6825 + '<label>' + this.opts.curLang.link + '</label>'
6826 + '<input type="text" id="redactor_file_link" class="redactor_input" />'
6827 + '<label><input type="checkbox" id="redactor_link_blank"> ' + this.opts.curLang.link_new_tab + '</label>'
6828 + '<label>' + this.opts.curLang.image_position + '</label>'
6829 + '<select id="redactor_form_image_align">'
6830 + '<option value="none">' + this.opts.curLang.none + '</option>'
6831 + '<option value="left">' + this.opts.curLang.left + '</option>'
6832 + '<option value="center">' + this.opts.curLang.center + '</option>'
6833 + '<option value="right">' + this.opts.curLang.right + '</option>'
6834 + '</select>'
6835 + '</section>'
6836 + '<footer>'
6837 + '<button id="redactor_image_delete_btn" class="redactor_modal_btn redactor_modal_delete_btn">' + this.opts.curLang._delete + '</button>'
6838 + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>'
6839 + '<button id="redactorSaveBtn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.save + '</button>'
6840 + '</footer>',
6841
6842 modal_image: String()
6843 + '<section id="redactor-modal-image-insert">'
6844 + '<div id="redactor_tabs">'
6845 + '<a href="#" id="redactor-tab-control-1" class="redactor_tabs_act">' + this.opts.curLang.upload + '</a>'
6846 + '<a href="#" id="redactor-tab-control-2">' + this.opts.curLang.choose + '</a>'
6847 + '<a href="#" id="redactor-tab-control-3">' + this.opts.curLang.link + '</a>'
6848 + '</div>'
6849 + '<div id="redactor-progress" class="redactor-progress-inline" style="display: none;"><span></span></div>'
6850 + '<form id="redactorInsertImageForm" method="post" action="" enctype="multipart/form-data">'
6851 + '<div id="redactor_tab1" class="redactor_tab">'
6852 + '<input type="file" id="redactor_file" name="' + this.opts.imageUploadParam + '" />'
6853 + '</div>'
6854 + '<div id="redactor_tab2" class="redactor_tab" style="display: none;">'
6855 + '<div id="redactor_image_box"></div>'
6856 + '</div>'
6857 + '</form>'
6858 + '<div id="redactor_tab3" class="redactor_tab" style="display: none;">'
6859 + '<label>' + this.opts.curLang.image_web_link + '</label>'
6860 + '<input type="text" name="redactor_file_link" id="redactor_file_link" class="redactor_input" /><br><br>'
6861 + '</div>'
6862 + '</section>'
6863 + '<footer>'
6864 + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>'
6865 + '<button class="redactor_modal_btn redactor_modal_action_btn" id="redactor_upload_btn">' + this.opts.curLang.insert + '</button>'
6866 + '</footer>',
6867
6868 modal_link: String()
6869 + '<section id="redactor-modal-link-insert">'
6870 + '<label>URL</label>'
6871 + '<input type="text" class="redactor_input" id="redactor_link_url" />'
6872 + '<label>' + this.opts.curLang.text + '</label>'
6873 + '<input type="text" class="redactor_input" id="redactor_link_url_text" />'
6874 + '<label><input type="checkbox" id="redactor_link_blank"> ' + this.opts.curLang.link_new_tab + '</label>'
6875 + '</section>'
6876 + '<footer>'
6877 + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>'
6878 + '<button id="redactor_insert_link_btn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.insert + '</button>'
6879 + '</footer>',
6880
6881 modal_table: String()
6882 + '<section id="redactor-modal-table-insert">'
6883 + '<label>' + this.opts.curLang.rows + '</label>'
6884 + '<input type="text" size="5" value="2" id="redactor_table_rows" />'
6885 + '<label>' + this.opts.curLang.columns + '</label>'
6886 + '<input type="text" size="5" value="3" id="redactor_table_columns" />'
6887 + '</section>'
6888 + '<footer>'
6889 + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>'
6890 + '<button id="redactor_insert_table_btn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.insert + '</button>'
6891 + '</footer>',
6892
6893 modal_video: String()
6894 + '<section id="redactor-modal-video-insert">'
6895 + '<form id="redactorInsertVideoForm">'
6896 + '<label>' + this.opts.curLang.video_html_code + '</label>'
6897 + '<textarea id="redactor_insert_video_area" style="width: 99%; height: 160px;"></textarea>'
6898 + '</form>'
6899 + '</section>'
6900 + '<footer>'
6901 + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>'
6902 + '<button id="redactor_insert_video_btn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.insert + '</button>'
6903 + '</footer>'
6904
6905 });
6906 },
6907 modalInit: function(title, content, width, callback)
6908 {
6909 var $redactorModalOverlay = $('#redactor_modal_overlay');
6910
6911 // modal overlay
6912 if (!$redactorModalOverlay.length)
6913 {
6914 this.$overlay = $redactorModalOverlay = $('<div id="redactor_modal_overlay" style="display: none;"></div>');
6915 $('body').prepend(this.$overlay);
6916 }
6917
6918 if (this.opts.modalOverlay)
6919 {
6920 $redactorModalOverlay.show().on('click', $.proxy(this.modalClose, this));
6921 }
6922
6923 var $redactorModal = $('#redactor_modal');
6924
6925 if (!$redactorModal.length)
6926 {
6927 this.$modal = $redactorModal = $('<div id="redactor_modal" style="display: none;"><div id="redactor_modal_close">&times;</div><header id="redactor_modal_header"></header><div id="redactor_modal_inner"></div></div>');
6928 $('body').append(this.$modal);
6929 }
6930
6931 $('#redactor_modal_close').on('click', $.proxy(this.modalClose, this));
6932
6933 this.hdlModalClose = $.proxy(function(e)
6934 {
6935 if (e.keyCode === this.keyCode.ESC)
6936 {
6937 this.modalClose();
6938 return false;
6939 }
6940
6941 }, this);
6942
6943 $(document).keyup(this.hdlModalClose);
6944 this.$editor.keyup(this.hdlModalClose);
6945
6946 // set content
6947 this.modalcontent = false;
6948 if (content.indexOf('#') == 0)
6949 {
6950 this.modalcontent = $(content);
6951 $('#redactor_modal_inner').empty().append(this.modalcontent.html());
6952 this.modalcontent.html('');
6953
6954 }
6955 else
6956 {
6957 $('#redactor_modal_inner').empty().append(content);
6958 }
6959
6960 $redactorModal.find('#redactor_modal_header').html(title);
6961
6962 // draggable
6963 if (typeof $.fn.draggable !== 'undefined')
6964 {
6965 $redactorModal.draggable({ handle: '#redactor_modal_header' });
6966 $redactorModal.find('#redactor_modal_header').css('cursor', 'move');
6967 }
6968
6969 var $redactor_tabs = $('#redactor_tabs');
6970
6971 // tabs
6972 if ($redactor_tabs.length )
6973 {
6974 var that = this;
6975 $redactor_tabs.find('a').each(function(i, s)
6976 {
6977 i++;
6978 $(s).on('click', function(e)
6979 {
6980 e.preventDefault();
6981
6982 $redactor_tabs.find('a').removeClass('redactor_tabs_act');
6983 $(this).addClass('redactor_tabs_act');
6984 $('.redactor_tab').hide();
6985 $('#redactor_tab' + i ).show();
6986 $('#redactor_tab_selected').val(i);
6987
6988 if (that.isMobile() === false)
6989 {
6990 var height = $redactorModal.outerHeight();
6991 $redactorModal.css('margin-top', '-' + (height + 10) / 2 + 'px');
6992 }
6993 });
6994 });
6995 }
6996
6997 $redactorModal.find('.redactor_btn_modal_close').on('click', $.proxy(this.modalClose, this));
6998
6999 var buttons = $redactorModal.find('footer button');
7000 var buttonsSize = buttons.size();
7001 if (buttonsSize > 0)
7002 {
7003 $(buttons).css('width', (width/buttonsSize) + 'px')
7004 }
7005
7006 // save scroll
7007 if (this.opts.autoresize === true)
7008 {
7009 this.saveModalScroll = this.document.body.scrollTop;
7010 }
7011 else
7012 {
7013 this.saveModalScroll = this.$editor.scrollTop();
7014 }
7015
7016 if (this.isMobile() === false)
7017 {
7018 $redactorModal.css({
7019 position: 'fixed',
7020 top: '-2000px',
7021 left: '50%',
7022 width: width + 'px',
7023 marginLeft: '-' + (width / 2) + 'px'
7024 }).show();
7025
7026 this.modalSaveBodyOveflow = $(document.body).css('overflow');
7027 $(document.body).css('overflow', 'hidden');
7028
7029 }
7030 else
7031 {
7032 $redactorModal.css({
7033 position: 'fixed',
7034 width: '100%',
7035 height: '100%',
7036 top: '0',
7037 left: '0',
7038 margin: '0',
7039 minHeight: '300px'
7040 }).show();
7041 }
7042
7043 // modal actions callback
7044 if (typeof callback === 'function')
7045 {
7046 callback();
7047 }
7048
7049 // modal shown callback
7050 setTimeout($.proxy(function()
7051 {
7052 this.callback('modalOpened');
7053
7054 }, this), 11);
7055
7056 // fix bootstrap modal focus
7057 $(document).off('focusin.modal');
7058
7059 if (this.isMobile() === false)
7060 {
7061 setTimeout(function()
7062 {
7063 var height = $redactorModal.outerHeight();
7064 $redactorModal.css({
7065 top: '50%',
7066 height: 'auto',
7067 minHeight: 'auto',
7068 marginTop: '-' + (height + 10) / 2 + 'px'
7069 });
7070 }, 10);
7071 }
7072
7073 $redactorModal.find('input[type=text]').keypress(function(e)
7074 {
7075 if (e.which === 13 )
7076 {
7077 $redactorModal.find('.redactor_modal_action_btn').click();
7078 e.preventDefault();
7079 }
7080 });
7081
7082 },
7083 modalClose: function()
7084 {
7085 $('#redactor_modal_close').off('click', this.modalClose);
7086 $('#redactor_modal').fadeOut('fast', $.proxy(function()
7087 {
7088 var redactorModalInner = $('#redactor_modal_inner');
7089
7090 if (this.modalcontent !== false)
7091 {
7092 this.modalcontent.html(redactorModalInner.html());
7093 this.modalcontent = false;
7094 }
7095
7096 redactorModalInner.html('');
7097
7098 if (this.opts.modalOverlay)
7099 {
7100 $('#redactor_modal_overlay').hide().off('click', this.modalClose);
7101 }
7102
7103 $(document).unbind('keyup', this.hdlModalClose);
7104 this.$editor.unbind('keyup', this.hdlModalClose);
7105
7106 this.selectionRestore();
7107
7108 // restore scroll
7109 if (this.opts.autoresize && this.saveModalScroll)
7110 {
7111 $(this.document.body).scrollTop(this.saveModalScroll);
7112 }
7113 else if (this.opts.autoresize === false && this.saveModalScroll)
7114 {
7115 this.$editor.scrollTop(this.saveModalScroll);
7116 }
7117
7118 this.callback('modalClosed');
7119
7120 }, this));
7121
7122
7123 if (this.isMobile() === false)
7124 {
7125 $(document.body).css('overflow', this.modalSaveBodyOveflow ? this.modalSaveBodyOveflow : 'visible');
7126 }
7127
7128 return false;
7129 },
7130 modalSetTab: function(num)
7131 {
7132 $('.redactor_tab').hide();
7133 $('#redactor_tabs').find('a').removeClass('redactor_tabs_act').eq(num - 1).addClass('redactor_tabs_act');
7134 $('#redactor_tab' + num).show();
7135 },
7136
7137 // S3
7138 s3handleFileSelect: function(e)
7139 {
7140 var files = e.target.files;
7141
7142 for (var i = 0, f; f = files[i]; i++)
7143 {
7144 this.s3uploadFile(f);
7145 }
7146 },
7147 s3uploadFile: function(file)
7148 {
7149 this.s3executeOnSignedUrl(file, $.proxy(function(signedURL)
7150 {
7151 this.s3uploadToS3(file, signedURL);
7152 }, this));
7153 },
7154 s3executeOnSignedUrl: function(file, callback)
7155 {
7156 var xhr = new XMLHttpRequest();
7157
7158 var mark = '?';
7159 if (this.opts.s3.search(/\?/) != '-1') mark = '&';
7160
7161 xhr.open('GET', this.opts.s3 + mark + 'name=' + file.name + '&type=' + file.type, true);
7162
7163 // Hack to pass bytes through unprocessed.
7164 if (xhr.overrideMimeType) xhr.overrideMimeType('text/plain; charset=x-user-defined');
7165
7166 xhr.onreadystatechange = function(e)
7167 {
7168 if (this.readyState == 4 && this.status == 200)
7169 {
7170 $('#redactor-progress').fadeIn();
7171 callback(decodeURIComponent(this.responseText));
7172 }
7173 else if(this.readyState == 4 && this.status != 200)
7174 {
7175 //setProgress(0, 'Could not contact signing script. Status = ' + this.status);
7176 }
7177 };
7178
7179 xhr.send();
7180 },
7181 s3createCORSRequest: function(method, url)
7182 {
7183 var xhr = new XMLHttpRequest();
7184 if ("withCredentials" in xhr)
7185 {
7186 xhr.open(method, url, true);
7187 }
7188 else if (typeof XDomainRequest != "undefined")
7189 {
7190 xhr = new XDomainRequest();
7191 xhr.open(method, url);
7192 }
7193 else
7194 {
7195 xhr = null;
7196 }
7197
7198 return xhr;
7199 },
7200 s3uploadToS3: function(file, url)
7201 {
7202 var xhr = this.s3createCORSRequest('PUT', url);
7203 if (!xhr)
7204 {
7205 //setProgress(0, 'CORS not supported');
7206 }
7207 else
7208 {
7209 xhr.onload = $.proxy(function()
7210 {
7211 if (xhr.status == 200)
7212 {
7213 //setProgress(100, 'Upload completed.');
7214
7215 $('#redactor-progress, #redactor-progress-drag').hide();
7216
7217 var s3image = url.split('?');
7218
7219 if (!s3image[0])
7220 {
7221 // url parsing is fail
7222 return false;
7223 }
7224
7225 this.selectionRestore();
7226
7227 var html = '';
7228 html = '<img id="image-marker" src="' + s3image[0] + '" />';
7229 if (this.opts.paragraphy) html = '<p>' + html + '</p>';
7230
7231 this.execCommand('inserthtml', html, false);
7232
7233 var image = $(this.$editor.find('img#image-marker'));
7234
7235 if (image.length) image.removeAttr('id');
7236 else image = false;
7237
7238 this.sync();
7239
7240 // upload image callback
7241 this.callback('imageUpload', image, false);
7242
7243 this.modalClose();
7244 this.observeImages();
7245
7246 }
7247 else
7248 {
7249 //setProgress(0, 'Upload error: ' + xhr.status);
7250 }
7251 }, this);
7252
7253 xhr.onerror = function()
7254 {
7255 //setProgress(0, 'XHR error.');
7256 };
7257
7258 xhr.upload.onprogress = function(e)
7259 {
7260 /*
7261 if (e.lengthComputable)
7262 {
7263 var percentLoaded = Math.round((e.loaded / e.total) * 100);
7264 setProgress(percentLoaded, percentLoaded == 100 ? 'Finalizing.' : 'Uploading.');
7265 }
7266 */
7267 };
7268
7269 xhr.setRequestHeader('Content-Type', file.type);
7270 xhr.setRequestHeader('x-amz-acl', 'public-read');
7271
7272 xhr.send(file);
7273 }
7274 },
7275
7276 // UPLOAD
7277 uploadInit: function(el, options)
7278 {
7279 this.uploadOptions = {
7280 url: false,
7281 success: false,
7282 error: false,
7283 start: false,
7284 trigger: false,
7285 auto: false,
7286 input: false
7287 };
7288
7289 $.extend(this.uploadOptions, options);
7290
7291 var $el = $('#' + el);
7292
7293 // Test input or form
7294 if ($el.length && $el[0].tagName === 'INPUT')
7295 {
7296 this.uploadOptions.input = $el;
7297 this.el = $($el[0].form);
7298 }
7299 else this.el = $el;
7300
7301 this.element_action = this.el.attr('action');
7302
7303 // Auto or trigger
7304 if (this.uploadOptions.auto)
7305 {
7306 $(this.uploadOptions.input).change($.proxy(function(e)
7307 {
7308 this.el.submit(function(e)
7309 {
7310 return false;
7311 });
7312
7313 this.uploadSubmit(e);
7314
7315 }, this));
7316
7317 }
7318 else if (this.uploadOptions.trigger)
7319 {
7320 $('#' + this.uploadOptions.trigger).click($.proxy(this.uploadSubmit, this));
7321 }
7322 },
7323 uploadSubmit: function(e)
7324 {
7325 $('#redactor-progress').fadeIn();
7326 this.uploadForm(this.element, this.uploadFrame());
7327 },
7328 uploadFrame: function()
7329 {
7330 this.id = 'f' + Math.floor(Math.random() * 99999);
7331
7332 var d = this.document.createElement('div');
7333 var iframe = '<iframe style="display:none" id="' + this.id + '" name="' + this.id + '"></iframe>';
7334
7335 d.innerHTML = iframe;
7336 $(d).appendTo("body");
7337
7338 // Start
7339 if (this.uploadOptions.start) this.uploadOptions.start();
7340
7341 $( '#' + this.id ).load($.proxy(this.uploadLoaded, this));
7342
7343 return this.id;
7344 },
7345 uploadForm: function(f, name)
7346 {
7347 if (this.uploadOptions.input)
7348 {
7349 var formId = 'redactorUploadForm' + this.id,
7350 fileId = 'redactorUploadFile' + this.id;
7351
7352 this.form = $('<form action="' + this.uploadOptions.url + '" method="POST" target="' + name + '" name="' + formId + '" id="' + formId + '" enctype="multipart/form-data" />');
7353
7354 // append hidden fields
7355 if (this.opts.uploadFields !== false && typeof this.opts.uploadFields === 'object')
7356 {
7357 $.each(this.opts.uploadFields, $.proxy(function(k, v)
7358 {
7359 if (v != null && v.toString().indexOf('#') === 0) v = $(v).val();
7360
7361 var hidden = $('<input/>', {
7362 'type': "hidden",
7363 'name': k,
7364 'value': v
7365 });
7366
7367 $(this.form).append(hidden);
7368
7369 }, this));
7370 }
7371
7372 var oldElement = this.uploadOptions.input;
7373 var newElement = $(oldElement).clone();
7374
7375 $(oldElement).attr('id', fileId).before(newElement).appendTo(this.form);
7376
7377 $(this.form).css('position', 'absolute')
7378 .css('top', '-2000px')
7379 .css('left', '-2000px')
7380 .appendTo('body');
7381
7382 this.form.submit();
7383
7384 }
7385 else
7386 {
7387 f.attr('target', name)
7388 .attr('method', 'POST')
7389 .attr('enctype', 'multipart/form-data')
7390 .attr('action', this.uploadOptions.url);
7391
7392 this.element.submit();
7393 }
7394 },
7395 uploadLoaded: function()
7396 {
7397 var i = $( '#' + this.id)[0], d;
7398
7399 if (i.contentDocument) d = i.contentDocument;
7400 else if (i.contentWindow) d = i.contentWindow.document;
7401 else d = window.frames[this.id].document;
7402
7403 // Success
7404 if (this.uploadOptions.success)
7405 {
7406 $('#redactor-progress').hide();
7407
7408 if (typeof d !== 'undefined')
7409 {
7410 // Remove bizarre <pre> tag wrappers around our json data:
7411 var rawString = d.body.innerHTML;
7412 var jsonString = rawString.match(/\{(.|\n)*\}/)[0];
7413
7414 jsonString = jsonString.replace(/^\[/, '');
7415 jsonString = jsonString.replace(/\]$/, '');
7416
7417 var json = $.parseJSON(jsonString);
7418
7419 if (typeof json.error == 'undefined') this.uploadOptions.success(json);
7420 else
7421 {
7422 this.uploadOptions.error(this, json);
7423 this.modalClose();
7424 }
7425 }
7426 else
7427 {
7428 this.modalClose();
7429 alert('Upload failed!');
7430 }
7431 }
7432
7433 this.el.attr('action', this.element_action);
7434 this.el.attr('target', '');
7435 },
7436
7437 // DRAGUPLOAD
7438 draguploadInit: function (el, options)
7439 {
7440 this.draguploadOptions = $.extend({
7441 url: false,
7442 success: false,
7443 error: false,
7444 preview: false,
7445 uploadFields: false,
7446 text: this.opts.curLang.drop_file_here,
7447 atext: this.opts.curLang.or_choose,
7448 uploadParam: false
7449 }, options);
7450
7451 if (window.FormData === undefined) return false;
7452
7453 this.droparea = $('<div class="redactor_droparea"></div>');
7454 this.dropareabox = $('<div class="redactor_dropareabox">' + this.draguploadOptions.text + '</div>');
7455 this.dropalternative = $('<div class="redactor_dropalternative">' + this.draguploadOptions.atext + '</div>');
7456
7457 this.droparea.append(this.dropareabox);
7458
7459 $(el).before(this.droparea);
7460 $(el).before(this.dropalternative);
7461
7462 // drag over
7463 this.dropareabox.on('dragover', $.proxy(function()
7464 {
7465 return this.draguploadOndrag();
7466
7467 }, this));
7468
7469 // drag leave
7470 this.dropareabox.on('dragleave', $.proxy(function()
7471 {
7472 return this.draguploadOndragleave();
7473
7474 }, this));
7475
7476 // drop
7477 this.dropareabox.get(0).ondrop = $.proxy(function(e)
7478 {
7479 e.preventDefault();
7480
7481 this.dropareabox.removeClass('hover').addClass('drop');
7482
7483 this.dragUploadAjax(this.draguploadOptions.url, e.dataTransfer.files[0], false, false, false, this.draguploadOptions.uploadParam);
7484
7485 }, this );
7486 },
7487 dragUploadAjax: function(url, file, directupload, progress, e, uploadParam)
7488 {
7489 if (!directupload)
7490 {
7491 var xhr = $.ajaxSettings.xhr();
7492 if (xhr.upload)
7493 {
7494 xhr.upload.addEventListener('progress', $.proxy(this.uploadProgress, this), false);
7495 }
7496
7497 $.ajaxSetup({
7498 xhr: function () { return xhr; }
7499 });
7500 }
7501
7502 // drop callback
7503 this.callback('drop', e);
7504
7505 var fd = new FormData();
7506
7507 // append file data
7508 if (uploadParam !== false)
7509 {
7510 fd.append(uploadParam, file);
7511 }
7512 else
7513 {
7514 fd.append('file', file);
7515 }
7516
7517 // append hidden fields
7518 if (this.opts.uploadFields !== false && typeof this.opts.uploadFields === 'object')
7519 {
7520 $.each(this.opts.uploadFields, $.proxy(function(k, v)
7521 {
7522 if (v != null && v.toString().indexOf('#') === 0) v = $(v).val();
7523 fd.append(k, v);
7524
7525 }, this));
7526 }
7527
7528 $.ajax({
7529 url: url,
7530 dataType: 'html',
7531 data: fd,
7532 cache: false,
7533 contentType: false,
7534 processData: false,
7535 type: 'POST',
7536 success: $.proxy(function(data)
7537 {
7538 data = data.replace(/^\[/, '');
7539 data = data.replace(/\]$/, '');
7540
7541 var json = (typeof data === 'string' ? $.parseJSON(data) : data);
7542
7543 if (directupload)
7544 {
7545 progress.fadeOut('slow', function()
7546 {
7547 $(this).remove();
7548 });
7549
7550 var $img = $('<img>');
7551 $img.attr('src', json.filelink).attr('id', 'drag-image-marker');
7552
7553 this.insertNodeToCaretPositionFromPoint(e, $img[0]);
7554
7555 var image = $(this.$editor.find('img#drag-image-marker'));
7556 if (image.length) image.removeAttr('id');
7557 else image = false;
7558
7559 this.sync();
7560 this.observeImages();
7561
7562 // upload callback
7563 if (image) this.callback('imageUpload', image, json);
7564
7565 // error callback
7566 if (typeof json.error !== 'undefined') this.callback('imageUploadError', json);
7567 }
7568 else
7569 {
7570 if (typeof json.error == 'undefined')
7571 {
7572 this.draguploadOptions.success(json);
7573 }
7574 else
7575 {
7576 this.draguploadOptions.error(this, json);
7577 this.draguploadOptions.success(false);
7578 }
7579 }
7580
7581 }, this)
7582 });
7583 },
7584 draguploadOndrag: function()
7585 {
7586 this.dropareabox.addClass('hover');
7587 return false;
7588 },
7589 draguploadOndragleave: function()
7590 {
7591 this.dropareabox.removeClass('hover');
7592 return false;
7593 },
7594 uploadProgress: function(e, text)
7595 {
7596 var percent = e.loaded ? parseInt(e.loaded / e.total * 100, 10) : e;
7597 this.dropareabox.text('Loading ' + percent + '% ' + (text || ''));
7598 },
7599
7600 // UTILS
7601 isMobile: function()
7602 {
7603 return /(iPhone|iPod|BlackBerry|Android)/.test(navigator.userAgent);
7604 },
7605 isIPad: function()
7606 {
7607 return /iPad/.test(navigator.userAgent);
7608 },
7609 normalize: function(str)
7610 {
7611 if (typeof(str) === 'undefined') return 0;
7612 return parseInt(str.replace('px',''), 10);
7613 },
7614 outerHtml: function(el)
7615 {
7616 return $('<div>').append($(el).eq(0).clone()).html();
7617 },
7618 stripHtml: function(html)
7619 {
7620 var tmp = document.createElement("DIV");
7621 tmp.innerHTML = html;
7622 return tmp.textContent || tmp.innerText || "";
7623 },
7624 isString: function(obj)
7625 {
7626 return Object.prototype.toString.call(obj) == '[object String]';
7627 },
7628 isEmpty: function(html)
7629 {
7630 html = html.replace(/&#x200b;|<br>|<br\/>|&nbsp;/gi, '');
7631 html = html.replace(/\s/g, '');
7632 html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
7633
7634 return html == '';
7635 },
7636 isIe11: function()
7637 {
7638 return !!navigator.userAgent.match(/Trident\/7\./);
7639 },
7640 browser: function(browser)
7641 {
7642 var ua = navigator.userAgent.toLowerCase();
7643 var match = /(opr)[\/]([\w.]+)/.exec( ua ) ||
7644 /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
7645 /(webkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
7646 /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
7647 /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
7648 /(msie) ([\w.]+)/.exec( ua ) ||
7649 ua.indexOf("trident") >= 0 && /(rv)(?::| )([\w.]+)/.exec( ua ) ||
7650 ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
7651 [];
7652
7653 if (browser == 'version') return match[2];
7654 if (browser == 'webkit') return (match[1] == 'chrome' || match[1] == 'webkit');
7655 if (match[1] == 'rv') return browser == 'msie';
7656 if (match[1] == 'opr') return browser == 'webkit';
7657
7658 return browser == match[1];
7659
7660 },
7661 oldIE: function()
7662 {
7663 if (this.browser('msie') && parseInt(this.browser('version'), 10) < 9) return true;
7664 return false;
7665 },
7666 getFragmentHtml: function (fragment)
7667 {
7668 var cloned = fragment.cloneNode(true);
7669 var div = this.document.createElement('div');
7670
7671 div.appendChild(cloned);
7672 return div.innerHTML;
7673 },
7674 extractContent: function()
7675 {
7676 var node = this.$editor[0];
7677 var frag = this.document.createDocumentFragment();
7678 var child;
7679
7680 while ((child = node.firstChild))
7681 {
7682 frag.appendChild(child);
7683 }
7684
7685 return frag;
7686 },
7687 isParentRedactor: function(el)
7688 {
7689 if (!el) return false;
7690 if (this.opts.iframe) return el;
7691
7692 if ($(el).parents('div.redactor_editor').length == 0 || $(el).hasClass('redactor_editor')) return false;
7693 else return el;
7694 },
7695 currentOrParentIs: function(tagName)
7696 {
7697 var parent = this.getParent(), current = this.getCurrent();
7698 return parent && parent.tagName === tagName ? parent : current && current.tagName === tagName ? current : false;
7699 },
7700 isEndOfElement: function()
7701 {
7702 var current = this.getBlock();
7703 var offset = this.getCaretOffset(current);
7704
7705 var text = $.trim($(current).text()).replace(/\n\r\n/g, '');
7706
7707 var len = text.length;
7708
7709 if (offset == len) return true;
7710 else return false;
7711 },
7712 isFocused: function()
7713 {
7714 var el, sel = this.getSelection();
7715
7716 if (sel && sel.rangeCount && sel.rangeCount > 0) el = sel.getRangeAt(0).startContainer;
7717 if (!el) return false;
7718 if (this.opts.iframe)
7719 {
7720 if (this.getCaretOffsetRange().equals()) return !this.$editor.is(el);
7721 else return true;
7722 }
7723
7724 return $(el).closest('div.redactor_editor').length != 0;
7725 },
7726 removeEmptyAttr: function (el, attr)
7727 {
7728 if ($(el).attr(attr) == '') $(el).removeAttr(attr);
7729 },
7730 removeFromArrayByValue: function(array, value)
7731 {
7732 var index = null;
7733
7734 while ((index = array.indexOf(value)) !== -1)
7735 {
7736 array.splice(index, 1);
7737 }
7738
7739 return array;
7740 }
7741
7742 };
7743
7744 // constructor
7745 Redactor.prototype.init.prototype = Redactor.prototype;
7746
7747 // LINKIFY
7748 $.Redactor.fn.formatLinkify = function(protocol, convertLinks, convertImageLinks, convertVideoLinks, linkSize)
7749 {
7750 var url1 = /(^|&lt;|\s)(www\..+?\..+?)([.),]?)(\s|\.\s+|\)|&gt;|$)/,
7751 url2 = /(^|&lt;|\s)(((https?|ftp):\/\/|mailto:).+?)([.),]?)(\s|\.\s+|\)|&gt;|$)/,
7752 urlImage = /(https?:\/\/.*\.(?:png|jpg|jpeg|gif))/gi,
7753 urlYoutube = /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig,
7754 urlVimeo = /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/;
7755
7756 var childNodes = (this.$editor ? this.$editor.get(0) : this).childNodes, i = childNodes.length;
7757 while (i--)
7758 {
7759 var n = childNodes[i];
7760 if (n.nodeType === 3)
7761 {
7762 var html = n.nodeValue;
7763
7764 // youtube & vimeo
7765 if (convertVideoLinks && html)
7766 {
7767 var iframeStart = '<iframe width="500" height="281" src="',
7768 iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
7769
7770 if (html.match(urlYoutube))
7771 {
7772 html = html.replace(urlYoutube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd);
7773 $(n).after(html).remove();
7774 }
7775 else if (html.match(urlVimeo))
7776 {
7777 html = html.replace(urlVimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd);
7778 $(n).after(html).remove();
7779 }
7780 }
7781
7782 // image
7783 if (convertImageLinks && html && html.match(urlImage))
7784 {
7785 html = html.replace(urlImage, '<img src="$1">');
7786
7787 $(n).after(html).remove();
7788 }
7789
7790 // link
7791 if (convertLinks && html && (html.match(url1) || html.match(url2)))
7792 {
7793 var found = true;
7794 var first = true;
7795
7796 while (found)
7797 {
7798 var href;
7799 var url = url1;
7800 var href1 = url1.exec(html);
7801 var href2 = url2.exec(html);
7802
7803 if (href1 && href1[2] && href2 && href2[2])
7804 {
7805 //process whichever came first sequentially *first*
7806 var index1 = html.indexOf(href1[2]);
7807 var index2 = html.indexOf(href2[2]);
7808 if (index1 < index2)
7809 {
7810 href = href1;
7811 url = url1;
7812 }
7813 else
7814 {
7815 href = href2;
7816 url = url2
7817 }
7818 }
7819 else if (href1 && href1[2])
7820 {
7821 href = href1;
7822 url = url1
7823 }
7824 else if (href2 && href2[2])
7825 {
7826 href = href2;
7827 url = url2
7828 }
7829
7830 found = (href && href.length);
7831 if (found)
7832 {
7833 href = href[2];
7834 }
7835
7836 if (found && href && href.length > linkSize)
7837 {
7838 href = href.substring(0, linkSize) + '...';
7839 }
7840
7841 if (first)
7842 {
7843 html = html.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
7844 }
7845
7846 if (found && href)
7847 {
7848 if (url == url1)
7849 {
7850 html = html.replace(url1, '$1<a href="' + protocol + '$2">' + $.trim(href) + '</a>$3$4')
7851 }
7852 else
7853 {
7854 html = html.replace(url2, '$1<a href="$2">' + $.trim(href) + '</a>$5$6');
7855 }
7856 }
7857
7858 first = false;
7859 }
7860
7861 $(n).after(html).remove();
7862 }
7863 }
7864 else if (n.nodeType === 1 && !/^(a|button|textarea)$/i.test(n.tagName))
7865 {
7866 $.Redactor.fn.formatLinkify.call(n, protocol, convertLinks, convertImageLinks, convertVideoLinks, linkSize);
7867 }
7868 }
7869 };
7870
7871 })(jQuery);