Properly remove formatting in nested elements
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / 3rdParty / redactor2 / redactor.js
1 /*
2 Redactor II
3 Version 2.99
4 Heavily modified version for WoltLab Suite.
5
6 http://imperavi.com/redactor/
7
8 Copyright (c) 2009-2017, Imperavi LLC.
9 License: http://imperavi.com/redactor/license/
10
11 Usage: $('#content').redactor();
12 */
13
14 (function ($) {
15 'use strict';
16
17 if (!Function.prototype.bind) {
18 Function.prototype.bind = function (scope) {
19 var fn = this;
20 return function () {
21 return fn.apply(scope);
22 };
23 };
24 }
25
26 var uuid = 0;
27
28 var Environment = null;
29 if (typeof window.require === 'function') {
30 // Load the Environment class for a better browser detection, guarded by a function check
31 // to avoid bricking any calls to this file in a different context.
32 require(['Environment'], function(Env) {
33 Environment = Env;
34 });
35 }
36
37 // Plugin
38 $.fn.redactor = function (options) {
39 var val = [];
40 var args = Array.prototype.slice.call(arguments, 1);
41
42 if (typeof options === 'string') {
43 this.each(function () {
44 var instance = $.data(this, 'redactor');
45 var func;
46
47 if (options.search(/\./) !== '-1') {
48 func = options.split('.');
49 if (typeof instance[func[0]] !== 'undefined') {
50 func = instance[func[0]][func[1]];
51 }
52 }
53 else {
54 func = instance[options];
55 }
56
57 if (typeof instance !== 'undefined' && $.isFunction(func)) {
58 var methodVal = func.apply(instance, args);
59 if (methodVal !== undefined && methodVal !== instance) {
60 val.push(methodVal);
61 }
62 }
63 else {
64 $.error('No such method "' + options + '" for Redactor');
65 }
66 });
67 }
68 else {
69 this.each(function () {
70 $.data(this, 'redactor', {});
71 $.data(this, 'redactor', Redactor(this, options));
72 });
73 }
74
75 if (val.length === 0) {
76 return this;
77 }
78 else if (val.length === 1) {
79 return val[0];
80 }
81 else {
82 return val;
83 }
84
85 };
86
87 // Initialization
88 function Redactor(el, options) {
89 return new Redactor.prototype.init(el, options);
90 }
91
92 // Options
93 $.Redactor = Redactor;
94 $.Redactor.VERSION = '2.99'; // Fake version
95 $.Redactor.modules = [
96 'air', // Unsupported module
97 'autosave', // Unsupported module
98 'block',
99 'buffer',
100 'build',
101 'button',
102 'caret',
103 'clean',
104 'code',
105 'core',
106 'detect',
107 'dropdown',
108 'events',
109 'file', // Unsupported module
110 'focus',
111 'image',
112 'indent',
113 'inline',
114 'insert',
115 'keydown',
116 'keyup',
117 'lang',
118 'line',
119 'link',
120 'linkify', // Unsupported module
121 'list',
122 'marker',
123 'modal',
124 'observe',
125 'offset',
126 'paragraphize',
127 'paste',
128 'placeholder', // Unsupported module
129 'progress', // Unsupported module
130 'selection',
131 'shortcuts',
132 'storage', // Unsupported module
133 'toolbar',
134 'upload', // Unsupported module
135 'uploads3', // Unsupported module
136 'utils',
137
138 'browser' // deprecated
139 ];
140
141 $.Redactor.settings = {};
142 $.Redactor.opts = {
143
144 // settings
145 animation: false,
146 lang: 'en',
147 direction: 'ltr',
148 spellcheck: true,
149 overrideStyles: true,
150 stylesClass: false,
151 scrollTarget: document,
152
153 focus: false,
154 focusEnd: false,
155
156 clickToEdit: false,
157 structure: false,
158
159 tabindex: false,
160
161 minHeight: false, // string
162 maxHeight: false, // string
163
164 maxWidth: false, // string
165
166 plugins: false, // array
167 callbacks: {},
168
169 placeholder: false,
170
171 linkify: true,
172 enterKey: true,
173
174 pastePlainText: false,
175 pasteImages: true,
176 pasteLinks: true,
177 pasteBlockTags: [
178 'pre',
179 'h1',
180 'h2',
181 'h3',
182 'h4',
183 'h5',
184 'h6',
185 'table',
186 'tbody',
187 'thead',
188 'tfoot',
189 'th',
190 'tr',
191 'td',
192 'ul',
193 'ol',
194 'li',
195 'blockquote',
196 'p',
197 'figure',
198 'figcaption'
199 ],
200 pasteInlineTags: [
201 'br',
202 'strong',
203 'ins',
204 'code',
205 'del',
206 'span',
207 'samp',
208 'kbd',
209 'sup',
210 'sub',
211 'mark',
212 'var',
213 'cite',
214 'small',
215 'b',
216 'u',
217 'em',
218 'i'
219 ],
220
221 preClass: false, // string
222 preSpaces: 4, // or false
223 tabAsSpaces: false, // true or number of spaces
224 tabKey: true,
225
226 autosave: false, // false or url
227 autosaveName: false,
228 autosaveFields: false,
229
230 imageUpload: null,
231 imageUploadParam: 'file',
232 imageUploadFields: false,
233 imageUploadForms: false,
234 imageTag: 'figure',
235 imageEditable: true,
236 imageCaption: true,
237
238 imagePosition: false,
239 imageResizable: false,
240 imageFloatMargin: '10px',
241
242 dragImageUpload: true,
243 multipleImageUpload: true,
244 clipboardImageUpload: true,
245
246 fileUpload: null,
247 fileUploadParam: 'file',
248 fileUploadFields: false,
249 fileUploadForms: false,
250 dragFileUpload: true,
251
252 s3: false,
253
254 linkNewTab: false,
255 linkTooltip: true,
256 linkNofollow: false,
257 linkSize: 30,
258 linkValidation: true,
259 pasteLinkTarget: false,
260
261 videoContainerClass: 'video-container',
262
263 toolbar: true,
264 toolbarFixed: true,
265 toolbarFixedTarget: document,
266 toolbarFixedTopOffset: 0, // pixels
267 toolbarExternal: false, // ID selector
268 toolbarOverflow: false,
269
270 air: false,
271 airWidth: false,
272
273 formatting: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
274 formattingAdd: false,
275
276 buttons: ['format', 'bold', 'italic', 'deleted', 'lists', 'image', 'file', 'link', 'horizontalrule'], // + 'horizontalrule', 'underline', 'ol', 'ul', 'indent', 'outdent'
277 buttonsTextLabeled: false,
278 buttonsHide: [],
279 buttonsHideOnMobile: [],
280
281 script: true,
282 removeNewlines: false,
283 removeComments: true,
284 replaceTags: {
285 'b': 'strong',
286 'i': 'em',
287 'strike': 'del'
288 },
289
290 keepStyleAttr: [], // tag name array
291 keepInlineOnEnter: false,
292
293 // shortcuts
294 shortcuts: {
295 'ctrl+shift+m, meta+shift+m': {func: 'inline.removeFormat'},
296 'ctrl+b, meta+b': {
297 func: 'inline.format',
298 params: ['bold']
299 },
300 'ctrl+i, meta+i': {
301 func: 'inline.format',
302 params: ['italic']
303 },
304 'ctrl+h, meta+h': {
305 func: 'inline.format',
306 params: ['superscript']
307 },
308 'ctrl+l, meta+l': {
309 func: 'inline.format',
310 params: ['subscript']
311 },
312 'ctrl+k, meta+k': {func: 'link.show'},
313 'ctrl+shift+7': {
314 func: 'list.toggle',
315 params: ['orderedlist']
316 },
317 'ctrl+shift+8': {
318 func: 'list.toggle',
319 params: ['unorderedlist']
320 }
321 },
322 shortcutsAdd: false,
323
324 activeButtons: ['deleted', 'italic', 'bold'],
325 activeButtonsStates: {
326 b: 'bold',
327 strong: 'bold',
328 i: 'italic',
329 em: 'italic',
330 del: 'deleted',
331 strike: 'deleted'
332 },
333
334 // private lang
335 langs: {
336 en: {
337
338 'format': 'Format',
339 'image': 'Image',
340 'file': 'File',
341 'link': 'Link',
342 'bold': 'Bold',
343 'italic': 'Italic',
344 'deleted': 'Strikethrough',
345 'underline': 'Underline',
346 'bold-abbr': 'B',
347 'italic-abbr': 'I',
348 'deleted-abbr': 'S',
349 'underline-abbr': 'U',
350 'lists': 'Lists',
351 'link-insert': 'Insert link',
352 'link-edit': 'Edit link',
353 'link-in-new-tab': 'Open link in new tab',
354 'unlink': 'Unlink',
355 'cancel': 'Cancel',
356 'close': 'Close',
357 'insert': 'Insert',
358 'save': 'Save',
359 'delete': 'Delete',
360 'text': 'Text',
361 'edit': 'Edit',
362 'title': 'Title',
363 'paragraph': 'Normal text',
364 'quote': 'Quote',
365 'code': 'Code',
366 'heading1': 'Heading 1',
367 'heading2': 'Heading 2',
368 'heading3': 'Heading 3',
369 'heading4': 'Heading 4',
370 'heading5': 'Heading 5',
371 'heading6': 'Heading 6',
372 'filename': 'Name',
373 'optional': 'optional',
374 'unorderedlist': 'Unordered List',
375 'orderedlist': 'Ordered List',
376 'outdent': 'Outdent',
377 'indent': 'Indent',
378 'horizontalrule': 'Line',
379 'upload-label': 'Drop file here or ',
380 'caption': 'Caption',
381
382 'bulletslist': 'Bullets',
383 'numberslist': 'Numbers',
384
385 'image-position': 'Position',
386 'none': 'None',
387 'left': 'Left',
388 'right': 'Right',
389 'center': 'Center',
390
391 'accessibility-help-label': 'Rich text editor'
392 }
393 },
394
395 // private
396 type: 'textarea', // textarea, div, inline, pre
397 inline: false,
398 inlineTags: [
399 'a',
400 'span',
401 'strong',
402 'strike',
403 'b',
404 'u',
405 'em',
406 'i',
407 'code',
408 'del',
409 'ins',
410 'samp',
411 'kbd',
412 'sup',
413 'sub',
414 'mark',
415 'var',
416 'cite',
417 'small'
418 ],
419 blockTags: [
420 'pre',
421 'ul',
422 'ol',
423 'li',
424 'p',
425 'h1',
426 'h2',
427 'h3',
428 'h4',
429 'h5',
430 'h6',
431 'dl',
432 'dt',
433 'dd',
434 'div',
435 'td',
436 'blockquote',
437 'output',
438 'figcaption',
439 'figure',
440 'address',
441 'section',
442 'header',
443 'footer',
444 'aside',
445 'article',
446 'iframe'
447 ],
448 paragraphize: true,
449 paragraphizeBlocks: [
450 'table',
451 'div',
452 'pre',
453 'form',
454 'ul',
455 'ol',
456 'h1',
457 'h2',
458 'h3',
459 'h4',
460 'h5',
461 'h6',
462 'dl',
463 'blockquote',
464 'figcaption',
465 'address',
466 'section',
467 'header',
468 'footer',
469 'aside',
470 'article',
471 'object',
472 'style',
473 'script',
474 'iframe',
475 'select',
476 'input',
477 'textarea',
478 'button',
479 'option',
480 'map',
481 'area',
482 'math',
483 'hr',
484 'fieldset',
485 'legend',
486 'hgroup',
487 'nav',
488 'figure',
489 'details',
490 'menu',
491 'summary',
492 'p'
493 ],
494 emptyHtml: '<p>&#x200b;</p>',
495 invisibleSpace: '&#x200b;',
496 emptyHtmlRendered: $('').html('​').html(),
497 imageTypes: ['image/png', 'image/jpeg', 'image/gif'],
498 userAgent: navigator.userAgent.toLowerCase(),
499 observe: {
500 dropdowns: []
501 },
502 regexps: {
503 linkyoutube: /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.\-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig,
504 linkvimeo: /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/,
505 linkimage: /((https?|www)[^\s]+\.)(jpe?g|png|gif)(\?[^\s-]+)?/ig,
506 url: /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/ig
507 }
508
509 };
510
511 // Functionality
512 Redactor.fn = $.Redactor.prototype = {
513
514 keyCode: {
515 BACKSPACE: 8,
516 DELETE: 46,
517 UP: 38,
518 DOWN: 40,
519 ENTER: 13,
520 SPACE: 32,
521 ESC: 27,
522 TAB: 9,
523 CTRL: 17,
524 META: 91,
525 SHIFT: 16,
526 ALT: 18,
527 RIGHT: 39,
528 LEFT: 37,
529 LEFT_WIN: 91
530 },
531
532 // =init
533 init: function (el, options) {
534 this.$element = $(el);
535 this.uuid = uuid++;
536 this.sBuffer = [];
537 this.sRebuffer = [];
538
539 this.loadOptions(options);
540 this.loadModules();
541
542 // click to edit
543 if (this.opts.clickToEdit && !this.$element.hasClass('redactor-click-to-edit')) {
544 return this.loadToEdit(options);
545 }
546 else if (this.$element.hasClass('redactor-click-to-edit')) {
547 this.$element.removeClass('redactor-click-to-edit');
548 }
549
550 // block & inline test tag regexp
551 this.reIsBlock = new RegExp('^(' + this.opts.blockTags.join('|').toUpperCase() + ')$', 'i');
552 this.reIsInline = new RegExp('^(' + this.opts.inlineTags.join('|').toUpperCase() + ')$', 'i');
553
554 // set up drag upload
555 this.opts.dragImageUpload = (this.opts.imageUpload === null) ? false : this.opts.dragImageUpload;
556 this.opts.dragFileUpload = (this.opts.fileUpload === null) ? false : this.opts.dragFileUpload;
557
558 // formatting storage
559 this.formatting = {};
560
561 // load lang
562 this.lang.load();
563
564 // extend shortcuts
565 $.extend(this.opts.shortcuts, this.opts.shortcutsAdd);
566
567 // set editor
568 this.$editor = this.$element;
569
570 // detect type of editor
571 this.detectType();
572
573 // start callback
574 this.core.callback('start');
575 this.core.callback('startToEdit');
576
577 // build
578 this.start = true;
579 this.build.start();
580
581 },
582 detectType: function () {
583 if (this.build.isInline() || this.opts.inline) {
584 this.opts.type = 'inline';
585 }
586 else if (this.build.isTag('DIV')) {
587 this.opts.type = 'div';
588 }
589 else if (this.build.isTag('PRE')) {
590 this.opts.type = 'pre';
591 }
592 },
593 loadToEdit: function (options) {
594
595 this.$element.on('click.redactor-click-to-edit', $.proxy(function () {
596 this.initToEdit(options);
597
598 }, this));
599
600 this.$element.addClass('redactor-click-to-edit');
601
602 return;
603 },
604 initToEdit: function (options) {
605 $.extend(options.callbacks, {
606 startToEdit: function () {
607 this.insert.node(this.marker.get(), false);
608 },
609 initToEdit: function () {
610 this.selection.restore();
611 this.clickToCancelStorage = this.code.get();
612
613 // cancel
614 $(this.opts.clickToCancel).off('.redactor-click-to-edit');
615 $(this.opts.clickToCancel).show().on('click.redactor-click-to-edit',
616 $.proxy(function (e) {
617 e.preventDefault();
618
619 this.core.destroy();
620 this.events.syncFire = false;
621 this.$element.html(this.clickToCancelStorage);
622 this.core.callback('cancel', this.clickToCancelStorage);
623 this.events.syncFire = true;
624 this.clickToCancelStorage = '';
625 $(this.opts.clickToCancel).hide();
626 $(this.opts.clickToSave).hide();
627
628 this.$element.on('click.redactor-click-to-edit',
629 $.proxy(function () {
630 this.initToEdit(options);
631 }, this)
632 );
633
634 this.$element.addClass('redactor-click-to-edit');
635
636 }, this)
637 );
638
639 // save
640 $(this.opts.clickToSave).off('.redactor-click-to-edit');
641 $(this.opts.clickToSave).show().on('click.redactor-click-to-edit',
642 $.proxy(function (e) {
643 e.preventDefault();
644
645 this.core.destroy();
646 this.core.callback('save', this.code.get());
647 $(this.opts.clickToCancel).hide();
648 $(this.opts.clickToSave).hide();
649 this.$element.on('click.redactor-click-to-edit',
650 $.proxy(function () {
651 this.initToEdit(options);
652 }, this)
653 );
654 this.$element.addClass('redactor-click-to-edit');
655
656 }, this)
657 );
658 }
659
660 });
661
662 this.$element.redactor(options);
663 this.$element.off('.redactor-click-to-edit');
664
665 },
666 loadOptions: function (options) {
667 var settings = {};
668
669 // check namespace
670 if (typeof $.Redactor.settings.namespace !== 'undefined') {
671 if (this.$element.hasClass($.Redactor.settings.namespace)) {
672 settings = $.Redactor.settings;
673 }
674 }
675 else {
676 settings = $.Redactor.settings;
677 }
678
679 this.opts = $.extend({}, $.Redactor.opts, this.$element.data(), options);
680
681 this.opts = $.extend({}, this.opts, settings);
682
683 },
684 getModuleMethods: function (object) {
685 return Object.getOwnPropertyNames(object).filter(function (property) {
686 return typeof object[property] === 'function';
687 });
688 },
689 loadModules: function () {
690 var len = $.Redactor.modules.length;
691 for (var i = 0; i < len; i++) {
692 this.bindModuleMethods($.Redactor.modules[i]);
693 }
694 },
695 bindModuleMethods: function (module) {
696 if (typeof this[module] === 'undefined') {
697 return;
698 }
699
700 // init module
701 this[module] = this[module]();
702
703 var methods = this.getModuleMethods(this[module]);
704 var len = methods.length;
705
706 // bind methods
707 for (var z = 0; z < len; z++) {
708 this[module][methods[z]] = this[module][methods[z]].bind(this);
709 }
710 },
711
712 // =air -- UNSUPPORTED MODULE
713 air: function () {
714 return {
715 enabled: false,
716 collapsed: function () {},
717 collapsedEnd: function () {},
718 build: function () {},
719 append: function () {},
720 createContainer: function () {},
721 show: function () {},
722 bindHide: function () {},
723 hide: function () {}
724 };
725 },
726
727 // =autosave -- UNSUPPORTED MODULE
728 autosave: function () {
729 return {
730 enabled: false,
731 html: false,
732 init: function () {},
733 is: function () {},
734 send: function () {},
735 getHiddenFields: function () {},
736 success: function () {},
737 disable: function () {}
738 };
739 },
740
741 // =block
742 block: function () {
743 return {
744 format: function (tag, attr, value, type) {
745 tag = (tag === 'quote') ? 'blockquote' : tag;
746
747 this.block.tags = [
748 'p',
749 'blockquote',
750 'pre',
751 'h1',
752 'h2',
753 'h3',
754 'h4',
755 'h5',
756 'h6',
757 'div',
758 'figure'
759 ];
760 if ($.inArray(tag, this.block.tags) === -1) {
761 return;
762 }
763
764 if (tag === 'p' && typeof attr === 'undefined') {
765 // remove all
766 attr = 'class';
767 }
768
769 this.buffer.set();
770
771 return (this.utils.isCollapsed()) ? this.block.formatCollapsed(tag,
772 attr,
773 value,
774 type
775 ) : this.block.formatUncollapsed(
776 tag,
777 attr,
778 value,
779 type
780 );
781 },
782 formatCollapsed: function (tag, attr, value, type) {
783 this.selection.save();
784
785 var block = this.selection.block();
786 var currentTag = block.tagName.toLowerCase();
787 if ($.inArray(currentTag, this.block.tags) === -1) {
788 this.selection.restore();
789 return;
790 }
791
792 var clearAllAttrs = false;
793 if (currentTag === tag && attr === undefined) {
794 tag = 'p';
795 clearAllAttrs = true;
796 }
797
798 if (clearAllAttrs) {
799 this.block.removeAllClass();
800 this.block.removeAllAttr();
801 }
802
803 var replaced;
804 if (currentTag === 'blockquote' && this.utils.isEndOfElement(block)) {
805 this.marker.remove();
806
807 replaced = document.createElement('p');
808 replaced.innerHTML = this.opts.invisibleSpace;
809
810 $(block).after(replaced);
811 this.caret.start(replaced);
812 var $last = $(block).children().last();
813
814 if ($last.length !== 0 && $last[0].tagName === 'BR') {
815 $last.remove();
816 }
817 }
818 else {
819 replaced = this.utils.replaceToTag(block, tag);
820 }
821
822 if (typeof attr === 'object') {
823 type = value;
824 for (var key in attr) {
825 replaced = this.block.setAttr(replaced, key, attr[key], type);
826 }
827 }
828 else {
829 replaced = this.block.setAttr(replaced, attr, value, type);
830 }
831
832 // trim pre
833 if (tag === 'pre' && replaced.length === 1) {
834 $(replaced).html($.trim($(replaced).html()));
835 }
836
837 this.selection.restore();
838 this.block.removeInlineTags(replaced);
839
840 return replaced;
841 },
842 formatUncollapsed: function (tag, attr, value, type) {
843 this.selection.save();
844
845 var replaced = [];
846 var blocks = this.selection.blocks();
847
848 if (blocks[0] && ($(blocks[0]).hasClass('redactor-in') || $(blocks[0]).hasClass(
849 'redactor-box'))) {
850 blocks = this.core.editor().find(this.opts.blockTags.join(', '));
851 }
852
853 var len = blocks.length;
854 for (var i = 0; i < len; i++) {
855 var currentTag = blocks[i].tagName.toLowerCase();
856 if ($.inArray(currentTag,
857 this.block.tags
858 ) !== -1 && currentTag !== 'figure') {
859 var block = this.utils.replaceToTag(blocks[i], tag);
860
861 if (typeof attr === 'object') {
862 type = value;
863 for (var key in attr) {
864 block = this.block.setAttr(block,
865 key,
866 attr[key],
867 type
868 );
869 }
870 }
871 else {
872 block = this.block.setAttr(block, attr, value, type);
873 }
874
875 replaced.push(block);
876 this.block.removeInlineTags(block);
877 }
878 }
879
880 this.selection.restore();
881
882 // combine pre
883 if (tag === 'pre' && replaced.length !== 0) {
884 var first = replaced[0];
885 $.each(replaced, function (i, s) {
886 if (i !== 0) {
887 $(first).append('\n' + $.trim(s.html()));
888 $(s).remove();
889 }
890 });
891
892 replaced = [];
893 replaced.push(first);
894 }
895
896 return replaced;
897 },
898 removeInlineTags: function (node) {
899 node = node[0] || node;
900
901 var tags = this.opts.inlineTags;
902 var blocks = ['PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
903
904 if ($.inArray(node.tagName, blocks) === -1) {
905 return;
906 }
907
908 if (node.tagName !== 'PRE') {
909 var index = tags.indexOf('a');
910 tags.splice(index, 1);
911 }
912
913 $(node).find(tags.join(',')).not('.redactor-selection-marker').contents().unwrap();
914 },
915 setAttr: function (block, attr, value, type) {
916 if (typeof attr === 'undefined') {
917 return block;
918 }
919
920 var func = (typeof type === 'undefined') ? 'replace' : type;
921
922 if (attr === 'class') {
923 block = this.block[func + 'Class'](value, block);
924 }
925 else {
926 if (func === 'remove') {
927 block = this.block[func + 'Attr'](attr, block);
928 }
929 else if (func === 'removeAll') {
930 block = this.block[func + 'Attr'](attr, block);
931 }
932 else {
933 block = this.block[func + 'Attr'](attr, value, block);
934 }
935 }
936
937 return block;
938
939 },
940 getBlocks: function (block) {
941 block = (typeof block === 'undefined') ? this.selection.blocks() : block;
942
943 if ($(block).hasClass('redactor-box')) {
944 var blocks = [];
945 var nodes = this.core.editor().children();
946 $.each(nodes, $.proxy(function (i, node) {
947 if (this.utils.isBlock(node)) {
948 blocks.push(node);
949 }
950
951 }, this));
952
953 return blocks;
954 }
955
956 return block;
957 },
958 replaceClass: function (value, block) {
959 return $(this.block.getBlocks(block)).removeAttr('class').addClass(value)[0];
960 },
961 toggleClass: function (value, block) {
962 return $(this.block.getBlocks(block)).toggleClass(value)[0];
963 },
964 addClass: function (value, block) {
965 return $(this.block.getBlocks(block)).addClass(value)[0];
966 },
967 removeClass: function (value, block) {
968 return $(this.block.getBlocks(block)).removeClass(value)[0];
969 },
970 removeAllClass: function (block) {
971 return $(this.block.getBlocks(block)).removeAttr('class')[0];
972 },
973 replaceAttr: function (attr, value, block) {
974 block = this.block.removeAttr(attr, block);
975
976 return $(block).attr(attr, value)[0];
977 },
978 toggleAttr: function (attr, value, block) {
979 block = this.block.getBlocks(block);
980
981 var self = this;
982 var returned = [];
983 $.each(block, function (i, s) {
984 var $el = $(s);
985 if ($el.attr(attr)) {
986 returned.push(self.block.removeAttr(attr, s));
987 }
988 else {
989 returned.push(self.block.addAttr(attr, value, s));
990 }
991 });
992
993 return returned;
994
995 },
996 addAttr: function (attr, value, block) {
997 return $(this.block.getBlocks(block)).attr(attr, value)[0];
998 },
999 removeAttr: function (attr, block) {
1000 return $(this.block.getBlocks(block)).removeAttr(attr)[0];
1001 },
1002 removeAllAttr: function (block) {
1003 block = this.block.getBlocks(block);
1004
1005 var returned = [];
1006 $.each(block, function (i, s) {
1007 if (typeof s.attributes !== 'undefined') {
1008 while (s.attributes.length) {
1009 s.removeAttribute(s.attributes[0].name);
1010 }
1011 }
1012
1013 returned.push(s);
1014 });
1015
1016 return returned;
1017 }
1018 };
1019 },
1020
1021 // buffer
1022 buffer: function () {
1023 return {
1024 set: function (type) {
1025 if (typeof type === 'undefined') {
1026 this.buffer.clear();
1027 }
1028
1029 if (typeof type === 'undefined' || type === 'undo') {
1030 this.buffer.setUndo();
1031 }
1032 else {
1033 this.buffer.setRedo();
1034 }
1035 },
1036 setUndo: function () {
1037 var saved = this.selection.saveInstant();
1038
1039 var last = this.sBuffer[this.sBuffer.length - 1];
1040 var current = this.core.editor().html();
1041
1042 var save = (typeof last !== 'undefined' && (last[0] === current)) ? false : true;
1043 if (save) {
1044 this.sBuffer.push([current, saved]);
1045 }
1046
1047 //this.selection.restore();
1048 },
1049 setRedo: function () {
1050 var saved = this.selection.saveInstant();
1051 this.sRebuffer.push([this.core.editor().html(), saved]);
1052 //this.selection.restore();
1053 },
1054 add: function () {
1055 this.sBuffer.push([this.core.editor().html(), 0]);
1056 },
1057 undo: function () {
1058 if (this.sBuffer.length === 0) {
1059 return;
1060 }
1061
1062 var buffer = this.sBuffer.pop();
1063
1064 this.buffer.set('redo');
1065 this.core.editor().html(buffer[0]);
1066
1067 this.selection.restoreInstant(buffer[1]);
1068 this.selection.restore();
1069 this.observe.load();
1070 },
1071 redo: function () {
1072 if (this.sRebuffer.length === 0) {
1073 return;
1074 }
1075
1076 var buffer = this.sRebuffer.pop();
1077
1078 this.buffer.set('undo');
1079 this.core.editor().html(buffer[0]);
1080
1081 this.selection.restoreInstant(buffer[1]);
1082 this.selection.restore();
1083 this.observe.load();
1084 },
1085 clear: function () {
1086 this.sRebuffer = [];
1087 }
1088 };
1089 },
1090
1091 // =build
1092 build: function () {
1093 return {
1094 start: function () {
1095 if (this.opts.type !== 'textarea') {
1096 throw new Error('Only `<textarea>` types are allowed.');
1097 }
1098
1099 this.build.startTextarea();
1100
1101 // set in
1102 this.build.setIn();
1103
1104 // set id
1105 this.build.setId();
1106
1107 // enable
1108 this.build.enableEditor();
1109
1110 // options
1111 this.build.setOptions();
1112
1113 // call
1114 this.build.callEditor();
1115
1116 },
1117 createContainerBox: function () {
1118 this.$box = $('<div class="redactor-box" role="application" />');
1119 },
1120 setIn: function () {
1121 this.core.editor().addClass('redactor-in');
1122 },
1123 setId: function () {
1124 var id = (this.opts.type === 'textarea') ? 'redactor-uuid-' + this.uuid : this.$element.attr(
1125 'id');
1126
1127 this.core.editor().attr('id',
1128 (typeof id === 'undefined') ? 'redactor-uuid-' + this.uuid : id
1129 );
1130 },
1131 getName: function () {
1132 var name = this.$element.attr('name');
1133
1134 return (typeof name === 'undefined') ? 'content-' + this.uuid : name;
1135 },
1136 buildTextarea: function () {},
1137 loadFromTextarea: function () {
1138 this.$editor = $('<div />');
1139
1140 // textarea
1141 this.$textarea = this.$element;
1142 this.$element.attr('name', this.build.getName());
1143
1144 // place
1145 this.$box.insertAfter(this.$element).append(this.$editor).append(this.$element);
1146
1147 this.build.setStartAttrs();
1148
1149 // styles
1150 this.$editor.addClass('redactor-layer');
1151 if (this.opts.overrideStyles) this.$editor.addClass('redactor-styles');
1152
1153 this.$element.hide();
1154
1155 this.$box.prepend('<span class="redactor-voice-label" id="redactor-voice-' + this.uuid + '" aria-hidden="false">' + this.lang.get(
1156 'accessibility-help-label') + '</span>');
1157
1158 },
1159 setStartAttrs: function () {
1160 this.$editor.attr({
1161 'aria-labelledby': 'redactor-voice-' + this.uuid,
1162 'role': 'presentation'
1163 });
1164 },
1165 startTextarea: function () {
1166 this.build.createContainerBox();
1167
1168 // load
1169 this.build.loadFromTextarea();
1170
1171 // set code
1172 this.code.start(this.core.textarea().val());
1173
1174 // set value
1175 this.core.textarea().val(this.clean.onSync(this.$editor.html()));
1176 },
1177 isTag: function (tag) {
1178 return (this.$element[0].tagName === tag);
1179 },
1180 isInline: function () {
1181 return (!this.build.isTag('TEXTAREA') && !this.build.isTag('DIV') && !this.build.isTag(
1182 'PRE'));
1183 },
1184 enableEditor: function () {
1185 this.core.editor().attr({'contenteditable': true});
1186 },
1187 setOptions: function () {
1188 // inline
1189 if (this.opts.type === 'inline') {
1190 this.opts.enterKey = false;
1191 }
1192
1193 // inline & pre
1194 if (this.opts.type === 'inline' || this.opts.type === 'pre') {
1195 this.opts.toolbarMobile = false;
1196 this.opts.toolbar = false;
1197
1198 }
1199
1200 // spellcheck
1201 this.core.editor().attr('spellcheck', this.opts.spellcheck);
1202
1203 // structure
1204 if (this.opts.structure) {
1205 this.core.editor().addClass('redactor-structure');
1206 }
1207
1208 // styles class
1209 if (this.opts.stylesClass) {
1210 this.core.editor().addClass(this.opts.stylesClass);
1211 }
1212
1213 // options sets only in textarea mode
1214 if (this.opts.type !== 'textarea') {
1215 return;
1216 }
1217
1218 // direction
1219 this.core.box().attr('dir', this.opts.direction);
1220 this.core.editor().attr('dir', this.opts.direction);
1221
1222 // tabindex
1223 if (this.opts.tabindex) {
1224 this.core.editor().attr('tabindex', this.opts.tabindex);
1225 }
1226
1227 // min height
1228 if (this.opts.minHeight) {
1229 this.core.editor().css('min-height', this.opts.minHeight);
1230 }
1231 else {
1232 this.core.editor().css('min-height', '40px');
1233 }
1234
1235 // max height
1236 if (this.opts.maxHeight) {
1237 this.core.editor().css('max-height', this.opts.maxHeight);
1238 }
1239
1240 // max width
1241 if (this.opts.maxWidth) {
1242 this.core.editor().css({
1243 'max-width': this.opts.maxWidth,
1244 'margin': 'auto'
1245 });
1246 }
1247
1248 },
1249 callEditor: function () {
1250 this.build.disableBrowsersEditing();
1251
1252 this.events.init();
1253 this.build.setHelpers();
1254
1255 // init buttons
1256 this.toolbarsButtons = this.button.init();
1257
1258 // load toolbar
1259 this.toolbar.build();
1260
1261 // observe dropdowns
1262 this.core.editor().on(
1263 'mouseup.redactor-observe.' + this.uuid + ' keyup.redactor-observe.' + this.uuid + ' focus.redactor-observe.' + this.uuid + ' touchstart.redactor-observe.' + this.uuid,
1264 $.proxy(this.observe.toolbar, this)
1265 );
1266 this.core.element().on('blur.callback.redactor', $.proxy(function () {
1267 this.button.setInactiveAll();
1268
1269 }, this));
1270
1271 // modal templates init
1272 this.modal.templates();
1273
1274 // plugins
1275 this.build.plugins();
1276
1277 // sync code
1278 this.code.html = this.code.cleaned(this.core.editor().html());
1279
1280 // init callback
1281 this.core.callback('init');
1282 this.core.callback('initToEdit');
1283
1284 // started
1285 this.start = false;
1286
1287 },
1288 setHelpers: function () {
1289 // focus
1290 if (this.opts.focus) {
1291 setTimeout(this.focus.start, 100);
1292 }
1293 else if (this.opts.focusEnd) {
1294 setTimeout(this.focus.end, 100);
1295 }
1296
1297 },
1298 disableBrowsersEditing: function () {
1299 try {
1300 // FF fix
1301 document.execCommand('enableObjectResizing', false, false);
1302 document.execCommand('enableInlineTableEditing', false, false);
1303 // IE prevent converting links
1304 document.execCommand('AutoUrlDetect', false, false);
1305 }
1306 catch (e) {}
1307 },
1308 plugins: function () {
1309 if (!this.opts.plugins) {
1310 return;
1311 }
1312
1313 $.each(this.opts.plugins, $.proxy(function (i, s) {
1314 var func = (typeof RedactorPlugins !== 'undefined' && typeof RedactorPlugins[s] !== 'undefined') ? RedactorPlugins : Redactor.fn;
1315
1316 if (!$.isFunction(func[s])) {
1317 return;
1318 }
1319
1320 this[s] = func[s]();
1321
1322 // get methods
1323 var methods = this.getModuleMethods(this[s]);
1324 var len = methods.length;
1325
1326 // bind methods
1327 for (var z = 0; z < len; z++) {
1328 this[s][methods[z]] = this[s][methods[z]].bind(this);
1329 }
1330
1331 // append lang
1332 if (typeof this[s].langs !== 'undefined') {
1333 var lang = {};
1334 if (typeof this[s].langs[this.opts.lang] !== 'undefined') {
1335 lang = this[s].langs[this.opts.lang];
1336 }
1337 else if (typeof this[s].langs[this.opts.lang] === 'undefined' && typeof this[s].langs.en !== 'undefined') {
1338 lang = this[s].langs.en;
1339 }
1340
1341 // extend
1342 var self = this;
1343 $.each(lang, function (i, s) {
1344 if (typeof self.opts.curLang[i] === 'undefined') {
1345 self.opts.curLang[i] = s;
1346 }
1347 });
1348 }
1349
1350 // init
1351 if (typeof this[s].init === 'function') {
1352 this[s].init();
1353 }
1354 }, this));
1355
1356 }
1357 };
1358 },
1359
1360 // =button
1361 button: function () {
1362 return {
1363 toolbar: function () {
1364 return (typeof this.button.$toolbar === 'undefined' || !this.button.$toolbar) ? this.$toolbar : this.button.$toolbar;
1365 },
1366 init: function () {
1367 return {
1368 format: {
1369 title: this.lang.get('format'),
1370 icon: true,
1371 dropdown: {
1372 p: {
1373 title: this.lang.get('paragraph'),
1374 func: 'block.format'
1375 },
1376 blockquote: {
1377 title: this.lang.get('quote'),
1378 func: 'block.format'
1379 },
1380 pre: {
1381 title: this.lang.get('code'),
1382 func: 'block.format'
1383 },
1384 h1: {
1385 title: this.lang.get('heading1'),
1386 func: 'block.format'
1387 },
1388 h2: {
1389 title: this.lang.get('heading2'),
1390 func: 'block.format'
1391 },
1392 h3: {
1393 title: this.lang.get('heading3'),
1394 func: 'block.format'
1395 },
1396 h4: {
1397 title: this.lang.get('heading4'),
1398 func: 'block.format'
1399 },
1400 h5: {
1401 title: this.lang.get('heading5'),
1402 func: 'block.format'
1403 },
1404 h6: {
1405 title: this.lang.get('heading6'),
1406 func: 'block.format'
1407 }
1408 }
1409 },
1410 bold: {
1411 title: this.lang.get('bold-abbr'),
1412 icon: true,
1413 label: this.lang.get('bold'),
1414 func: 'inline.format'
1415 },
1416 italic: {
1417 title: this.lang.get('italic-abbr'),
1418 icon: true,
1419 label: this.lang.get('italic'),
1420 func: 'inline.format'
1421 },
1422 deleted: {
1423 title: this.lang.get('deleted-abbr'),
1424 icon: true,
1425 label: this.lang.get('deleted'),
1426 func: 'inline.format'
1427 },
1428 underline: {
1429 title: this.lang.get('underline-abbr'),
1430 icon: true,
1431 label: this.lang.get('underline'),
1432 func: 'inline.format'
1433 },
1434 lists: {
1435 title: this.lang.get('lists'),
1436 icon: true,
1437 dropdown: {
1438 unorderedlist: {
1439 title: '&bull; ' + this.lang.get('unorderedlist'),
1440 func: 'list.toggle'
1441 },
1442 orderedlist: {
1443 title: '1. ' + this.lang.get('orderedlist'),
1444 func: 'list.toggle'
1445 },
1446 outdent: {
1447 title: '< ' + this.lang.get('outdent'),
1448 func: 'indent.decrease',
1449 observe: {
1450 element: 'li',
1451 out: {
1452 attr: {
1453 'class': 'redactor-dropdown-link-inactive',
1454 'aria-disabled': true
1455 }
1456 }
1457 }
1458 },
1459 indent: {
1460 title: '> ' + this.lang.get('indent'),
1461 func: 'indent.increase',
1462 observe: {
1463 element: 'li',
1464 out: {
1465 attr: {
1466 'class': 'redactor-dropdown-link-inactive',
1467 'aria-disabled': true
1468 }
1469 }
1470 }
1471 }
1472 }
1473 },
1474 ul: {
1475 title: '&bull; ' + this.lang.get('bulletslist'),
1476 icon: true,
1477 func: 'list.toggle'
1478 },
1479 ol: {
1480 title: '1. ' + this.lang.get('numberslist'),
1481 icon: true,
1482 func: 'list.toggle'
1483 },
1484 outdent: {
1485 title: this.lang.get('outdent'),
1486 icon: true,
1487 func: 'indent.decrease'
1488 },
1489 indent: {
1490 title: this.lang.get('indent'),
1491 icon: true,
1492 func: 'indent.increase'
1493 },
1494 image: {
1495 title: this.lang.get('image'),
1496 icon: true,
1497 func: 'image.show'
1498 },
1499 file: {
1500 title: this.lang.get('file'),
1501 icon: true,
1502 func: 'file.show'
1503 },
1504 link: {
1505 title: this.lang.get('link'),
1506 icon: true,
1507 dropdown: {
1508 link: {
1509 title: this.lang.get('link-insert'),
1510 func: 'link.show',
1511 observe: {
1512 element: 'a',
1513 'in': {
1514 title: this.lang.get('link-edit')
1515 },
1516 out: {
1517 title: this.lang.get(
1518 'link-insert')
1519 }
1520 }
1521 },
1522 unlink: {
1523 title: this.lang.get('unlink'),
1524 func: 'link.unlink',
1525 observe: {
1526 element: 'a',
1527 out: {
1528 attr: {
1529 'class': 'redactor-dropdown-link-inactive',
1530 'aria-disabled': true
1531 }
1532 }
1533 }
1534 }
1535 }
1536 },
1537 horizontalrule: {
1538 title: this.lang.get('horizontalrule'),
1539 icon: true,
1540 func: 'line.insert'
1541 }
1542 };
1543 },
1544 setFormatting: function () {
1545 for (var key in this.toolbarsButtons.format.dropdown) {
1546 if (this.toolbarsButtons.format.dropdown.hasOwnProperty(key) && this.opts.formatting.indexOf(key) === -1) {
1547 delete this.toolbarsButtons.format.dropdown[key];
1548 }
1549 }
1550
1551 },
1552 hideButtons: function () {
1553 if (this.opts.buttonsHide.length !== 0) {
1554 this.button.hideButtonsSlicer(this.opts.buttonsHide);
1555 }
1556 },
1557 hideButtonsOnMobile: function () {
1558 if (this.detect.isMobile() && this.opts.buttonsHideOnMobile.length !== 0) {
1559 this.button.hideButtonsSlicer(this.opts.buttonsHideOnMobile);
1560 }
1561 },
1562 hideButtonsSlicer: function (buttons) {
1563 $.each(buttons, $.proxy(function (i, s) {
1564 var index = this.opts.buttons.indexOf(s);
1565 if (index !== -1) {
1566 this.opts.buttons.splice(index, 1);
1567 }
1568
1569 }, this));
1570 },
1571 load: function ($toolbar) {
1572 this.button.buttons = [];
1573
1574 this.opts.buttons.forEach((function(btnName) {
1575 if (btnName === 'image' && !this.image.is()) {
1576 return;
1577 }
1578
1579 if (this.toolbarsButtons.hasOwnProperty(btnName)) {
1580 var listItem = elCreate('li');
1581 listItem.appendChild(this.button.build(btnName,
1582 this.toolbarsButtons[btnName]
1583 )[0]);
1584 $toolbar[0].appendChild(listItem);
1585 }
1586 }).bind(this));
1587 },
1588 buildButtonTooltip: function () {},
1589 build: function (btnName, btnObject) {
1590 var $button = $('<a href="javascript:void(null);" rel="' + btnName + '" />');
1591
1592 $button.addClass('re-button re-' + btnName);
1593 $button.attr({
1594 'role': 'button',
1595 'tabindex': '-1'
1596 });
1597
1598 $button.html(btnObject.title);
1599
1600 // click
1601 if (btnObject.func || btnObject.command || btnObject.dropdown) {
1602 this.button.setEvent($button, btnName, btnObject);
1603 }
1604
1605 // dropdown
1606 if (btnObject.dropdown) {
1607 $button.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup',
1608 true
1609 );
1610
1611 var $dropdown = $('<ul class="dropdownMenu redactor-dropdown-menu redactor-dropdown-menu-' + $button[0].rel + '" data-dropdown-allow-flip="horizontal" data-dropdown-ignore-page-scroll="true" />');
1612 $button.data('dropdown', $dropdown);
1613 this.dropdown.build(btnName, $dropdown, btnObject.dropdown);
1614
1615 this.button.setupDropdown($button[0], $dropdown[0]);
1616 }
1617
1618 this.button.buttons.push($button);
1619
1620 return $button;
1621 },
1622 setupDropdown: function(button, dropdown) {
1623 require(['Ui/SimpleDropdown'], (function (UiSimpleDropdown) {
1624 UiSimpleDropdown.initFragment(button, dropdown);
1625 UiSimpleDropdown.registerCallback(button.id, (function(containerId, action) {
1626 if (action === 'close') {
1627 this.dropdown.hideOut();
1628 }
1629 }).bind(this));
1630
1631 elData(button, 'a11y-mouse-event', 'mousedown');
1632 elData(button, 'aria-expanded', false);
1633
1634 button.addEventListener('click', function (event) {
1635 event.preventDefault();
1636 event.stopPropagation();
1637 });
1638 }).bind(this));
1639 },
1640 getButtons: function () {
1641 return this.button.toolbar().find('a.re-button');
1642 },
1643 getButtonsKeys: function () {
1644 return this.button.buttons;
1645 },
1646 setEvent: function ($button, btnName, btnObject) {
1647 $button.on('mousedown', $.proxy(function (e) {
1648 e.preventDefault();
1649
1650 if ($button.hasClass('redactor-button-disabled')) {
1651 return false;
1652 }
1653
1654 var type = 'func';
1655 var callback = btnObject.func;
1656
1657 if (btnObject.command) {
1658 type = 'command';
1659 callback = btnObject.command;
1660 }
1661 else if (btnObject.dropdown) {
1662 type = 'dropdown';
1663 callback = false;
1664 }
1665
1666 this.button.toggle(e, btnName, type, callback);
1667
1668 return false;
1669
1670 }, this));
1671 },
1672 toggle: function (e, btnName, type, callback, args) {
1673
1674 if (this.detect.isIe() || !this.detect.isDesktop()) {
1675 this.utils.freezeScroll();
1676 e.returnValue = false;
1677 }
1678
1679 if (type === 'command') {
1680 this.inline.format(callback);
1681 }
1682 else if (type === 'dropdown') {
1683 this.dropdown.show(e, btnName);
1684 }
1685 else {
1686 this.button.clickCallback(e, callback, btnName, args);
1687 }
1688
1689 if (type !== 'dropdown') {
1690 this.dropdown.hideAll(false);
1691 }
1692
1693 if (this.detect.isIe() || !this.detect.isDesktop()) {
1694 this.utils.unfreezeScroll();
1695 }
1696 },
1697 clickCallback: function (e, callback, btnName, args) {
1698 var func;
1699
1700 if (e instanceof Event) {
1701 e.preventDefault();
1702 }
1703 else if (e && e.originalEvent) {
1704 e.originalEvent.preventDefault();
1705 }
1706
1707 args = (typeof args === 'undefined') ? btnName : args;
1708
1709 if ($.isFunction(callback)) {
1710 callback.call(this, btnName);
1711 }
1712 else if (callback.search(/\./) !== '-1') {
1713 func = callback.split('.');
1714 if (typeof this[func[0]] === 'undefined') {
1715 return;
1716 }
1717
1718 if (typeof args === 'object') {
1719 this[func[0]][func[1]].apply(this, args);
1720 }
1721 else {
1722 this[func[0]][func[1]].call(this, args);
1723 }
1724 }
1725 else {
1726
1727 if (typeof args === 'object') {
1728 this[callback].apply(this, args);
1729 }
1730 else {
1731 this[callback].call(this, args);
1732 }
1733 }
1734
1735 this.observe.buttons(e, btnName);
1736
1737 },
1738 all: function () {
1739 return this.button.buttons;
1740 },
1741 get: function (key) {
1742 if (this.opts.toolbar === false) {
1743 return;
1744 }
1745
1746 return this.button.toolbar().find('a.re-' + key);
1747 },
1748 set: function (key, title) {
1749 if (this.opts.toolbar === false) {
1750 return;
1751 }
1752
1753 var $btn = this.button.toolbar().find('a.re-' + key);
1754
1755 $btn.html(title).attr('aria-label', title);
1756
1757 return $btn;
1758 },
1759 add: function (key, title) {
1760 if (this.button.isAdded(key) !== true) {
1761 return $();
1762 }
1763
1764 var btn = this.button.build(key, {title: title});
1765
1766 this.button.toolbar().append($('<li>').append(btn));
1767
1768 return btn;
1769 },
1770 addFirst: function (key, title) {
1771 if (this.button.isAdded(key) !== true) {
1772 return $();
1773 }
1774
1775 var btn = this.button.build(key, {title: title});
1776
1777 this.button.toolbar().prepend($('<li>').append(btn));
1778
1779 return btn;
1780 },
1781 addAfter: function (afterkey, key, title) {
1782 if (this.button.isAdded(key) !== true) {
1783 return $();
1784 }
1785
1786 var btn = this.button.build(key, {title: title});
1787 var $btn = this.button.get(afterkey);
1788
1789 if ($btn.length !== 0) {
1790 $btn.parent().after($('<li>').append(btn));
1791 }
1792 else {
1793 this.button.toolbar().append($('<li>').append(btn));
1794 }
1795
1796 return btn;
1797 },
1798 addBefore: function (beforekey, key, title) {
1799 if (this.button.isAdded(key) !== true) {
1800 return $();
1801 }
1802
1803 var btn = this.button.build(key, {title: title});
1804 var $btn = this.button.get(beforekey);
1805
1806 if ($btn.length !== 0) {
1807 $btn.parent().before($('<li>').append(btn));
1808 }
1809 else {
1810 this.button.toolbar().append($('<li>').append(btn));
1811 }
1812
1813 return btn;
1814 },
1815 isAdded: function (key) {
1816 var index = this.opts.buttonsHideOnMobile.indexOf(key);
1817 if (this.opts.toolbar === false || (index !== -1 && this.detect.isMobile())) {
1818 return false;
1819 }
1820
1821 return true;
1822 },
1823 setIcon: function ($btn, icon) {
1824 if (!this.opts.buttonsTextLabeled) {
1825 $btn.html(icon).addClass('re-button-icon');
1826 }
1827 },
1828 changeIcon: function (key, newIconClass) {
1829 var $btn = this.button.get(key);
1830 if ($btn.length !== 0) {
1831 $btn.find('i').removeAttr('class').addClass('re-icon-' + newIconClass);
1832 }
1833 },
1834 addCallback: function ($btn, callback) {
1835 if (typeof $btn === 'undefined' || this.opts.toolbar === false) {
1836 return;
1837 }
1838
1839 var type = (callback === 'dropdown') ? 'dropdown' : 'func';
1840 var key = $btn.attr('rel');
1841 $btn.on('mousedown', $.proxy(function (e) {
1842 if ($btn.hasClass('redactor-button-disabled')) {
1843 return false;
1844 }
1845
1846 this.button.toggle(e, key, type, callback);
1847
1848 }, this));
1849 },
1850 addDropdown: function ($btn, dropdown) {
1851 if (this.opts.toolbar === false) {
1852 return;
1853 }
1854
1855 $btn.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true);
1856
1857 var key = $btn.attr('rel');
1858 this.button.addCallback($btn, 'dropdown');
1859
1860 var $dropdown = $('<ul class="dropdownMenu redactor-dropdown-menu redactor-dropdown-menu-' + key + '" data-dropdown-allow-flip="horizontal" data-dropdown-ignore-page-scroll="true" />');
1861 $btn.data('dropdown', $dropdown);
1862
1863 // build dropdown
1864 if (dropdown) {
1865 this.dropdown.build(key, $dropdown, dropdown);
1866
1867 this.button.setupDropdown($btn[0], $dropdown[0]);
1868 }
1869
1870 return $dropdown;
1871 },
1872 setActive: function (key) {
1873 this.button.get(key).addClass('redactor-act').attr({
1874 'aria-pressed': true,
1875 tabindex: 0
1876 });
1877 },
1878 setInactive: function (key) {
1879 this.button.get(key).removeClass('redactor-act').attr({
1880 'aria-pressed': false,
1881 tabindex: (key === 'html') ? 0 : -1
1882 });
1883 },
1884 setInactiveAll: function (key) {
1885 var $btns = this.button.toolbar().find('a.re-button');
1886
1887 if (typeof key !== 'undefined') {
1888 $btns = $btns.not('.re-' + key);
1889 }
1890
1891 $btns.removeClass('redactor-act').attr({
1892 'aria-pressed': false,
1893 tabindex: (key === 'html') ? 0 : -1
1894 });
1895 },
1896 disable: function (key) {
1897 this.button.get(key).addClass('redactor-button-disabled').attr('aria-disabled', true);
1898 },
1899 enable: function (key) {
1900 this.button.get(key).removeClass('redactor-button-disabled').attr('aria-disabled', false);
1901 },
1902 disableAll: function (key) {
1903 var $btns = this.button.toolbar().find('a.re-button');
1904 if (typeof key !== 'undefined') {
1905 if (!Array.isArray(key)) {
1906 key = [key];
1907 }
1908
1909 key = key.map(function(value) {
1910 return '.re-' + value
1911 });
1912
1913 $btns = $btns.not(key.join(','));
1914 }
1915
1916 $btns.addClass('redactor-button-disabled').attr('aria-disabled', true);
1917 },
1918 enableAll: function () {
1919 this.button.toolbar().find('a.re-button').removeClass('redactor-button-disabled').attr('aria-disabled', false);
1920 },
1921 remove: function (key) {
1922 this.button.get(key).remove();
1923 }
1924 };
1925 },
1926
1927 // =caret
1928 caret: function () {
1929 return {
1930 set: function (node1, node2, end) {
1931 var cs = this.core.editor().scrollTop();
1932 this.core.editor().focus();
1933 this.core.editor().scrollTop(cs);
1934
1935 end = (typeof end === 'undefined') ? 0 : 1;
1936
1937 node1 = node1[0] || node1;
1938 node2 = node2[0] || node2;
1939
1940 var sel = this.selection.get();
1941 var range = this.selection.range(sel);
1942
1943 try {
1944 range.setStart(node1, 0);
1945 range.setEnd(node2, end);
1946 }
1947 catch (e) {}
1948
1949 this.selection.update(sel, range);
1950 },
1951 prepare: function (node) {
1952 // firefox focus
1953 if (this.detect.isFirefox() && typeof this.start !== 'undefined') {
1954 this.core.editor().focus();
1955 }
1956
1957 return node[0] || node;
1958 },
1959 start: function (node) {
1960 var sel, range;
1961 node = this.caret.prepare(node);
1962
1963 if (!node) {
1964 return;
1965 }
1966
1967 if (node.tagName === 'BR') {
1968 return this.caret.before(node);
1969 }
1970
1971 var $first = $(node).children().first();
1972
1973 // empty or inline tag
1974 var inline = this.utils.isInlineTag(node.tagName);
1975 if (node.innerHTML === '' || inline) {
1976 this.caret.setStartEmptyOrInline(node, inline);
1977 }
1978 // empty inline inside
1979 else if ($first && $first.length !== 0 && this.utils.isInlineTag($first[0].tagName) && $first.text() === '') {
1980 this.caret.setStartEmptyOrInline($first[0], true);
1981 }
1982 // block tag
1983 else {
1984 sel = window.getSelection();
1985 sel.removeAllRanges();
1986
1987 range = document.createRange();
1988 range.selectNodeContents(node);
1989 range.collapse(true);
1990 sel.addRange(range);
1991 }
1992
1993 },
1994 setStartEmptyOrInline: function (node, inline) {
1995 var sel = window.getSelection();
1996 var range = document.createRange();
1997 var textNode = document.createTextNode('\u200B');
1998
1999 range.setStart(node, 0);
2000 range.insertNode(textNode);
2001 range.setStartAfter(textNode);
2002 range.collapse(true);
2003
2004 sel.removeAllRanges();
2005 sel.addRange(range);
2006
2007 // remove invisible text node
2008 if (!inline) {
2009 this.core.editor().on('keydown.redactor-remove-textnode', function () {
2010 $(textNode).remove();
2011 $(this).off('keydown.redactor-remove-textnode');
2012 });
2013 }
2014 },
2015 end: function (node) {
2016 var sel, range;
2017 node = this.caret.prepare(node);
2018
2019 if (!node) {
2020 return;
2021 }
2022
2023 // empty node
2024 if (node.tagName !== 'BR' && node.innerHTML === '') {
2025 return this.caret.start(node);
2026 }
2027
2028 // br
2029 if (node.tagName === 'BR') {
2030 var space = document.createElement('span');
2031 space.className = 'redactor-invisible-space';
2032 space.innerHTML = '&#x200b;';
2033
2034 $(node).after(space);
2035
2036 sel = window.getSelection();
2037 sel.removeAllRanges();
2038
2039 range = document.createRange();
2040
2041 range.setStartBefore(space);
2042 range.setEndBefore(space);
2043 sel.addRange(range);
2044
2045 $(space).replaceWith(function () {
2046 return $(this).contents();
2047 });
2048
2049 return;
2050 }
2051
2052 if (node.lastChild && node.lastChild.nodeType === 1) {
2053 return this.caret.after(node.lastChild);
2054 }
2055
2056 var sel = window.getSelection();
2057 if (sel.getRangeAt || sel.rangeCount) {
2058 try {
2059 var range = sel.getRangeAt(0);
2060 range.selectNodeContents(node);
2061 range.collapse(false);
2062
2063 sel.removeAllRanges();
2064 sel.addRange(range);
2065 }
2066 catch (e) {}
2067 }
2068 },
2069 after: function (node) {
2070 var sel, range;
2071 node = this.caret.prepare(node);
2072
2073 if (!node) {
2074 return;
2075 }
2076
2077 if (node.tagName === 'BR') {
2078 return this.caret.end(node);
2079 }
2080
2081 // block tag
2082 if (this.utils.isBlockTag(node.tagName)) {
2083 var next = this.caret.next(node);
2084
2085 if (typeof next === 'undefined') {
2086 this.caret.end(node);
2087 }
2088 else {
2089 // table
2090 if (next.tagName === 'TABLE') {
2091 next = $(next).find('th, td').first()[0];
2092 }
2093 // list
2094 else if (next.tagName === 'UL' || next.tagName === 'OL') {
2095 next = $(next).find('li').first()[0];
2096 }
2097
2098 this.caret.start(next);
2099 }
2100
2101 return;
2102 }
2103
2104 // inline tag
2105 var textNode = document.createTextNode('\u200B');
2106
2107 sel = window.getSelection();
2108 sel.removeAllRanges();
2109
2110 range = document.createRange();
2111 range.setStartAfter(node);
2112 range.insertNode(textNode);
2113 range.setStartAfter(textNode);
2114 range.collapse(true);
2115
2116 sel.addRange(range);
2117
2118 },
2119 before: function (node) {
2120 var sel, range;
2121 node = this.caret.prepare(node);
2122
2123 if (!node) {
2124 return;
2125 }
2126
2127 // block tag
2128 if (this.utils.isBlockTag(node.tagName)) {
2129 var prev = this.caret.prev(node);
2130
2131 if (typeof prev === 'undefined') {
2132 this.caret.start(node);
2133 }
2134 else {
2135 // table
2136 if (prev.tagName === 'TABLE') {
2137 prev = $(prev).find('th, td').last()[0];
2138 }
2139 // list
2140 else if (prev.tagName === 'UL' || prev.tagName === 'OL') {
2141 prev = $(prev).find('li').last()[0];
2142 }
2143
2144 this.caret.end(prev);
2145 }
2146
2147 return;
2148 }
2149
2150 // inline tag
2151 sel = window.getSelection();
2152 sel.removeAllRanges();
2153
2154 range = document.createRange();
2155
2156 range.setStartBefore(node);
2157 range.collapse(true);
2158
2159 sel.addRange(range);
2160 },
2161 next: function (node) {
2162 var $next = $(node).next();
2163 if ($next.hasClass('redactor-script-tag, redactor-selection-marker')) {
2164 return $next.next()[0];
2165 }
2166 else {
2167 return $next[0];
2168 }
2169 },
2170 prev: function (node) {
2171 var $prev = $(node).prev();
2172 if ($prev.hasClass('redactor-script-tag, redactor-selection-marker')) {
2173 return $prev.prev()[0];
2174 }
2175 else {
2176 return $prev[0];
2177 }
2178 },
2179
2180 // #backward
2181 offset: function (node) {
2182 return this.offset.get(node);
2183 }
2184
2185 };
2186 },
2187
2188 // =clean
2189 clean: function () {
2190 return {
2191 onSet: function (html) {
2192 html = this.clean.savePreCode(html);
2193
2194 // convert script tag
2195 if (this.opts.script) {
2196 html = html.replace(
2197 /<script(.*?[^>]?)>([\w\W]*?)<\/script>/gi,
2198 '<pre class="redactor-script-tag" $1>$2</pre>'
2199 );
2200 }
2201
2202 // converting entity
2203 html = html.replace(/\$/g, '&#36;');
2204 html = html.replace(/&amp;/g, '&');
2205
2206 // replace special characters in links
2207 html = html.replace(/<a href="(.*?[^>]?)®(.*?[^>]?)">/gi,
2208 '<a href="$1&reg$2">'
2209 );
2210
2211 // save markers
2212 html = html.replace(/<span id="selection-marker-1"(.*?[^>]?)>​<\/span>/gi,
2213 '###marker1###'
2214 );
2215 html = html.replace(/<span id="selection-marker-2"(.*?[^>]?)>​<\/span>/gi,
2216 '###marker2###'
2217 );
2218
2219 // replace tags
2220 var self = this;
2221 var $div = $('<div/>').html($.parseHTML(html, document, true));
2222
2223 var replacement = this.opts.replaceTags;
2224 if (replacement) {
2225 var keys = Object.keys(this.opts.replaceTags);
2226 $div.find(keys.join(',')).each(function (i, s) {
2227 self.utils.replaceToTag(s,
2228 replacement[s.tagName.toLowerCase()]
2229 );
2230 });
2231 }
2232
2233 // add span marker
2234 $div.find('span, a').attr('data-redactor-span', true);
2235
2236 // add style cache
2237 $div.find(this.opts.inlineTags.join(',')).each(function () {
2238 // add style cache
2239 var $el = $(this);
2240
2241 if ($el.attr('style')) {
2242 $el.attr('data-redactor-style-cache', $el.attr('style'));
2243 }
2244 });
2245
2246 html = $div.html();
2247
2248 // remove tags
2249 var tags = ['font', 'html', 'head', 'link', 'body', 'meta', 'applet'];
2250 if (!this.opts.script) {
2251 tags.push('script');
2252 }
2253
2254 html = this.clean.stripTags(html, tags);
2255
2256 // remove html comments
2257 if (this.opts.removeComments) {
2258 html = html.replace(/<!--[\s\S]*?-->/gi, '');
2259 }
2260
2261 // paragraphize
2262 html = this.paragraphize.load(html);
2263
2264 // restore markers
2265 html = html.replace(
2266 '###marker1###',
2267 '<span id="selection-marker-1" class="redactor-selection-marker">​</span>'
2268 );
2269 html = html.replace(
2270 '###marker2###',
2271 '<span id="selection-marker-2" class="redactor-selection-marker">​</span>'
2272 );
2273
2274 // empty
2275 if (html.search(/^(||\s||<br\s?\/?>||&nbsp;)$/i) !== -1) {
2276 return this.opts.emptyHtml;
2277 }
2278
2279 return html;
2280 },
2281 onGet: function (html) {
2282 return this.clean.onSync(html);
2283 },
2284 onSync: function (html) {
2285 // remove invisible spaces
2286 html = html.replace(/\u200B/g, '');
2287 html = html.replace(/&#x200b;/gi, '');
2288 //html = html.replace(/&nbsp;&nbsp;/gi, '&nbsp;');
2289
2290 if (html.search(/^<p>(||\s||<br\s?\/?>||&nbsp;)<\/p>$/i) !== -1) {
2291 return '';
2292 }
2293
2294 // remove image resize
2295 html = html.replace(
2296 /<span(.*?)id="redactor-image-box"(.*?[^>])>([\w\W]*?)<img(.*?)><\/span>/gi,
2297 '$3<img$4>'
2298 );
2299 html = html.replace(/<span(.*?)id="redactor-image-resizer"(.*?[^>])>(.*?)<\/span>/gi,
2300 ''
2301 );
2302 html = html.replace(/<span(.*?)id="redactor-image-editter"(.*?[^>])>(.*?)<\/span>/gi,
2303 ''
2304 );
2305 html = html.replace(
2306 /<img(.*?)style="(.*?)opacity: 0\.5;(.*?)"(.*?)>/gi,
2307 '<img$1style="$2$3"$4>'
2308 );
2309
2310 var $div = $('<div/>').html($.parseHTML(html, document, true));
2311
2312 // remove empty atributes
2313 $div.find('*[style=""]').removeAttr('style');
2314 $div.find('*[class=""]').removeAttr('class');
2315 $div.find('*[rel=""]').removeAttr('rel');
2316 $div.find('*[data-image=""]').removeAttr('data-image');
2317 $div.find('*[alt=""]').removeAttr('alt');
2318 $div.find('*[title=""]').removeAttr('title');
2319 $div.find('*[data-redactor-style-cache]').removeAttr('data-redactor-style-cache');
2320
2321 // remove markers
2322 $div.find('.redactor-invisible-space, .redactor-unlink').each(function () {
2323 $(this).contents().unwrap();
2324 });
2325
2326 // remove span without attributes & span marker
2327 $div.find('span, a').removeAttr('data-redactor-span data-redactor-style-cache').each(
2328 function () {
2329 if (this.attributes.length === 0) {
2330 $(this).contents().unwrap();
2331 }
2332 });
2333
2334 // remove rel attribute from img
2335 $div.find('img').removeAttr('rel');
2336
2337 $div.find('.redactor-selection-marker, #redactor-insert-marker').remove();
2338
2339 html = $div.html();
2340
2341 // reconvert script tag
2342 if (this.opts.script) {
2343 html = html.replace(
2344 /<pre class="redactor-script-tag"(.*?[^>]?)>([\w\W]*?)<\/pre>/gi,
2345 '<script$1>$2</script>'
2346 );
2347 }
2348
2349 // restore form tag
2350 html = this.clean.restoreFormTags(html);
2351
2352 // remove br in|of li/header tags
2353 html = html.replace(new RegExp('<br\\s?/?></h', 'gi'), '</h');
2354 html = html.replace(new RegExp('<br\\s?/?></li>', 'gi'), '</li>');
2355 html = html.replace(new RegExp('</li><br\\s?/?>', 'gi'), '</li>');
2356
2357 // pre class
2358 html = html.replace(/<pre>/gi, '<pre>\n');
2359 if (this.opts.preClass) {
2360 html = html.replace(/<pre>/gi,
2361 '<pre class="' + this.opts.preClass + '">'
2362 );
2363 }
2364
2365 // link nofollow
2366 if (this.opts.linkNofollow) {
2367 html = html.replace(/<a(.*?)rel="nofollow"(.*?[^>])>/gi, '<a$1$2>');
2368 html = html.replace(/<a(.*?[^>])>/gi, '<a$1 rel="nofollow">');
2369 }
2370
2371 // replace special characters
2372 var chars = {
2373 '\u2122': '&trade;',
2374 '\u00a9': '&copy;',
2375 '\u2026': '&hellip;',
2376 '\u2014': '&mdash;',
2377 '\u2010': '&dash;'
2378 };
2379
2380 $.each(chars, function (i, s) {
2381 html = html.replace(new RegExp(i, 'g'), s);
2382 });
2383
2384 html = html.replace(/&amp;/g, '&');
2385
2386 // remove empty paragpraphs
2387 //html = html.replace(/<p><\/p>/gi, "");
2388
2389 // remove new lines
2390 html = html.replace(/\n{2,}/g, '\n');
2391
2392 // remove all newlines
2393 if (this.opts.removeNewlines) {
2394 html = html.replace(/\r?\n/g, '');
2395 }
2396
2397 return html;
2398 },
2399 onPaste: function (html, data, insert) {
2400 // if paste event
2401 if (insert !== true) {
2402 // remove google docs markers
2403 html = html.replace(/<b\sid="internal-source-marker(.*?)">([\w\W]*?)<\/b>/gi,
2404 '$2'
2405 );
2406 html = html.replace(/<b(.*?)id="docs-internal-guid(.*?)">([\w\W]*?)<\/b>/gi,
2407 '$3'
2408 );
2409
2410 // google docs styles
2411 html = html.replace(
2412 /<span[^>]*(font-style: italic; font-weight: bold|font-weight: bold; font-style: italic)[^>]*>([\w\W]*?)<\/span>/gi,
2413 '<b><i>$2</i></b>'
2414 );
2415 html = html.replace(
2416 /<span[^>]*(font-style: italic; font-weight: 700|font-weight: 700; font-style: italic)[^>]*>([\w\W]*?)<\/span>/gi,
2417 '<b><i>$2</i></b>'
2418 );
2419 html = html.replace(
2420 /<span[^>]*font-style: italic[^>]*>([\w\W]*?)<\/span>/gi,
2421 '<i>$1</i>'
2422 );
2423 html = html.replace(
2424 /<span[^>]*font-weight: bold[^>]*>([\w\W]*?)<\/span>/gi,
2425 '<b>$1</b>'
2426 );
2427 html = html.replace(
2428 /<span[^>]*font-weight: 700[^>]*>([\w\W]*?)<\/span>/gi,
2429 '<b>$1</b>'
2430 );
2431
2432 // op tag
2433 html = html.replace(/<o:p[^>]*>/gi, '');
2434 html = html.replace(/<\/o:p>/gi, '');
2435
2436 var msword = this.clean.isHtmlMsWord(html);
2437 if (msword) {
2438 html = this.clean.cleanMsWord(html);
2439 }
2440 }
2441
2442 html = $.trim(html);
2443
2444 if (data.pre) {
2445 if (this.opts.preSpaces) {
2446 html = html.replace(/\t/g,
2447 new Array(this.opts.preSpaces + 1).join(' ')
2448 );
2449 }
2450 }
2451 else {
2452
2453 html = this.clean.replaceBrToNl(html);
2454 html = this.clean.removeTagsInsidePre(html);
2455 }
2456
2457 // if paste event
2458 if (insert !== true) {
2459 html = this.clean.removeEmptyInlineTags(html);
2460
2461 if (data.encode === false) {
2462 html = html.replace(/&/g, '&amp;');
2463 html = this.clean.convertTags(html, data);
2464 html = this.clean.getPlainText(html);
2465 html = this.clean.reconvertTags(html, data);
2466 }
2467
2468 }
2469
2470 if (data.text) {
2471 html = this.clean.replaceNbspToSpaces(html);
2472 html = this.clean.getPlainText(html);
2473 }
2474
2475 if (data.lists) {
2476 html = html.replace('\n', '<br>');
2477 }
2478
2479 if (data.encode) {
2480 html = this.clean.encodeHtml(html);
2481 }
2482
2483 if (data.paragraphize) {
2484 // ff bugfix
2485 html = html.replace(/ \n/g, ' ');
2486 html = html.replace(/\n /g, ' ');
2487
2488 html = this.paragraphize.load(html);
2489
2490 // remove empty p
2491 html = html.replace(/<p><\/p>/g, '');
2492 }
2493
2494 // remove paragraphs form lists (google docs bug)
2495 html = html.replace(/<li><p>/g, '<li>');
2496 html = html.replace(/<\/p><\/li>/g, '</li>');
2497
2498 return html;
2499
2500 },
2501 getCurrentType: function (html, insert) {
2502 var blocks = this.selection.blocks();
2503
2504 var data = {
2505 text: false,
2506 encode: false,
2507 paragraphize: true,
2508 line: this.clean.isHtmlLine(html),
2509 blocks: this.clean.isHtmlBlocked(html),
2510 pre: false,
2511 lists: false,
2512 block: true,
2513 inline: true,
2514 links: true,
2515 images: true
2516 };
2517
2518 if (blocks.length === 1 && this.utils.isCurrentOrParent([
2519 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'figcaption'
2520 ])) {
2521 data.text = true;
2522 data.paragraphize = false;
2523 data.inline = false;
2524 data.images = false;
2525 data.links = false;
2526 data.line = true;
2527 }
2528 else if (this.opts.type === 'inline' || this.opts.enterKey === false) {
2529 data.paragraphize = false;
2530 data.block = false;
2531 data.line = true;
2532 }
2533 else if (blocks.length === 1 && this.utils.isCurrentOrParent(['li'])) {
2534 data.lists = true;
2535 data.block = false;
2536 data.paragraphize = false;
2537 data.images = false;
2538 }
2539 else if (blocks.length === 1 && this.utils.isCurrentOrParent([
2540 'th', 'td', 'blockquote'
2541 ])) {
2542 data.block = false;
2543 data.paragraphize = false;
2544
2545 }
2546 else if (this.opts.type === 'pre' || (blocks.length === 1 && this.utils.isCurrentOrParent(
2547 'pre'))) {
2548 data.inline = false;
2549 data.block = false;
2550 data.encode = true;
2551 data.pre = true;
2552 data.paragraphize = false;
2553 data.images = false;
2554 data.links = false;
2555 }
2556
2557 if (data.line === true) {
2558 data.paragraphize = false;
2559 }
2560
2561 if (insert === true) {
2562 data.text = false;
2563 }
2564
2565 return data;
2566
2567 },
2568 isHtmlBlocked: function (html) {
2569 var match1 = html.match(new RegExp('</(' + this.opts.blockTags.join('|').toUpperCase() + ')>',
2570 'gi'
2571 ));
2572 var match2 = html.match(new RegExp('<hr(.*?[^>])>', 'gi'));
2573
2574 return (match1 === null && match2 === null) ? false : true;
2575 },
2576 isHtmlLine: function (html) {
2577 if (this.clean.isHtmlBlocked(html)) {
2578 return false;
2579 }
2580
2581 var matchBR = html.match(/<br\s?\/?>/gi);
2582 var matchNL = html.match(/\n/gi);
2583
2584 return (!matchBR && !matchNL) ? true : false;
2585 },
2586 isHtmlMsWord: function (html) {
2587 return html.match(
2588 /class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i);
2589 },
2590 removeEmptyInlineTags: function (html) {
2591 var tags = this.opts.inlineTags;
2592 var $div = $('<div/>').html($.parseHTML(html, document, true));
2593 var self = this;
2594
2595 var $spans = $div.find('span');
2596 var $tags = $div.find(tags.join(','));
2597
2598 $tags.removeAttr('style');
2599
2600 $tags.each(function () {
2601 var tagHtml = $(this).html();
2602 if (this.attributes.length === 0 && self.utils.isEmpty(tagHtml)) {
2603 $(this).replaceWith(function () {
2604 return $(this).contents();
2605 });
2606 }
2607 });
2608
2609 $spans.each(function () {
2610 var tagHtml = $(this).html();
2611 if (this.attributes.length === 0) {
2612 $(this).replaceWith(function () {
2613 return $(this).contents();
2614 });
2615 }
2616 });
2617
2618 html = $div.html();
2619
2620 // convert php tags
2621 html = html.replace('<!--?php', '<?php');
2622 html = html.replace('<!--?', '<?');
2623 html = html.replace('?-->', '?>');
2624
2625 $div.remove();
2626
2627 return html;
2628 },
2629 cleanMsWord: function (html) {
2630 html = html.replace(/<!--[\s\S]*?-->/g, '');
2631 html = html.replace(/<o:p>[\s\S]*?<\/o:p>/gi, '');
2632 html = html.replace(/\n/g, ' ');
2633 html = html.replace(/<\/p>/gi, '</p><p><br data-redactor="br"></p>');
2634 html = html.replace(/<\/div>|<\/li>|<\/td>/gi, '\n\n');
2635
2636 var $div = $('<div/>').html(html);
2637
2638 // Find any <p> that contains a <br> and split it into separate paragraphs.
2639 /** @var {Element} br */
2640 elBySelAll('br', $div[0], function(br) {
2641 if (elData(br, 'redactor') === 'br') {
2642 br.removeAttribute('data-redactor');
2643 }
2644 else {
2645 var parent = br.parentNode;
2646 if (parent && parent.nodeName === 'P') {
2647 var paragraph = elCreate('p');
2648 while (br.nextSibling) {
2649 paragraph.appendChild(br.nextSibling);
2650 }
2651 parent.parentNode.insertBefore(paragraph, parent.nextSibling);
2652 elRemove(br);
2653 }
2654 }
2655 });
2656
2657 // lists
2658 var lastList = false;
2659 var lastLevel = 1;
2660 var listsIds = [];
2661
2662 $div.find('p[style]').each(function () {
2663 var matches = $(this).attr('style').match(
2664 /mso\-list\:l([0-9]+)\slevel([0-9]+)/);
2665
2666 if (matches) {
2667 var currentList = parseInt(matches[1]);
2668 var currentLevel = parseInt(matches[2]);
2669 var listType = $(this).html().match(/^[\w]+\./) ? 'ol' : 'ul';
2670
2671 var $li = $('<li/>').html($(this).html());
2672
2673 $li.html($li.html().replace(/^([\w\.]+)</, '<'));
2674 $li.find('span:first').remove();
2675
2676 if (currentLevel == 1 && $.inArray(currentList,
2677 listsIds
2678 ) == -1) {
2679 var $list = $('<' + listType + '/>').attr({
2680 'data-level': currentLevel,
2681 'data-list': currentList
2682 }).html($li);
2683 $(this).replaceWith($list);
2684
2685 lastList = currentList;
2686 listsIds.push(currentList);
2687 }
2688 else {
2689 if (currentLevel > lastLevel) {
2690 var $prevList = $div.find('[data-level="' + lastLevel + '"][data-list="' + lastList + '"]');
2691 var $lastList = $prevList;
2692
2693 for (var i = lastLevel; i < currentLevel; i++) {
2694 $list = $('<' + listType + '/>');
2695 $list.appendTo($lastList.find('li').last());
2696
2697 $lastList = $list;
2698 }
2699
2700 $lastList.attr({
2701 'data-level': currentLevel,
2702 'data-list': currentList
2703 }).html($li);
2704
2705 }
2706 else {
2707 var $prevList = $div.find('[data-level="' + currentLevel + '"][data-list="' + currentList + '"]').last();
2708
2709 $prevList.append($li);
2710 }
2711
2712 lastLevel = currentLevel;
2713 lastList = currentList;
2714
2715 $(this).remove();
2716 }
2717 }
2718 });
2719
2720 $div.find('[data-level][data-list]').removeAttr('data-level data-list');
2721
2722 // Find lists at the top level that are surrounded by `<p><br></p>`.
2723 elBySelAll('ol, ul', $div[0], function(list) {
2724 ['nextElementSibling', 'previousElementSibling'].forEach(function(property) {
2725 var sibling = list[property];
2726 while (sibling && sibling.nodeName === 'P' && sibling.className === '') {
2727 if (sibling.innerHTML !== '<br>') {
2728 break;
2729 }
2730
2731 elRemove(sibling);
2732 sibling = list[property];
2733 }
2734 });
2735 });
2736
2737 html = $div.html();
2738
2739 return html;
2740 },
2741 replaceNbspToSpaces: function (html) {
2742 return html.replace('&nbsp;', ' ');
2743 },
2744 replaceBrToNl: function (html) {
2745 return html.replace(/<br\s?\/?>/gi, '\n');
2746 },
2747 replaceNlToBr: function (html) {
2748 return html.replace(/\n/g, '<br />');
2749 },
2750 convertTags: function (html, data) {
2751 var $div = $('<div>').html(html);
2752
2753 // remove iframe
2754 $div.find('iframe').remove();
2755
2756 // link target & attrs
2757 var $links = $div.find('a');
2758 $links.removeAttr('style');
2759 if (this.opts.pasteLinkTarget !== false) {
2760 $links.attr('target', this.opts.pasteLinkTarget);
2761 }
2762
2763 // links
2764 if (data.links && this.opts.pasteLinks) {
2765 $div.find('a').each(function (i, link) {
2766 if (link.href) {
2767 var tmp = '#####[a href="' + link.href + '"';
2768 var attr;
2769 for (var j = 0, length = link.attributes.length; j < length; j++) {
2770 attr = link.attributes.item(j);
2771 if (attr.name !== 'href') {
2772 tmp += ' ' + attr.name + '="' + attr.value + '"';
2773 }
2774 }
2775
2776 link.outerHTML = tmp + ']#####' + link.innerHTML + '#####[/a]#####';
2777 }
2778 });
2779 }
2780
2781 html = $div.html();
2782
2783 // images
2784 if (data.images && this.opts.pasteImages) {
2785 html = html.replace(
2786 /<img(.*?)src="(.*?)"(.*?[^>])>/gi,
2787 '#####[img$1src="$2"$3]#####'
2788 );
2789 }
2790
2791 // plain text
2792 if (this.opts.pastePlainText) {
2793 return html;
2794 }
2795
2796 // all tags
2797 var blockTags = (data.lists) ? ['ul', 'ol', 'li'] : this.opts.pasteBlockTags;
2798
2799 var tags;
2800 if (data.block || data.lists) {
2801 tags = (data.inline) ? blockTags.concat(this.opts.pasteInlineTags) : blockTags;
2802 }
2803 else {
2804 tags = (data.inline) ? this.opts.pasteInlineTags : [];
2805 }
2806
2807 var len = tags.length;
2808 for (var i = 0; i < len; i++) {
2809 html = html.replace(new RegExp('<\/' + tags[i] + '>', 'gi'),
2810 '###/' + tags[i] + '###'
2811 );
2812
2813 if (tags[i] === 'td' || tags[i] === 'th') {
2814 html = html.replace(new RegExp(
2815 '<' + tags[i] + '(.*?[^>])((colspan|rowspan)="(.*?[^>])")?(.*?[^>])>',
2816 'gi'
2817 ), '###' + tags[i] + ' $2###');
2818 }
2819 else if (this.utils.isInlineTag(tags[i])) {
2820 html = html.replace(
2821 new RegExp('<' + tags[i] + '([^>]*)class="([^>]*)"[^>]*>',
2822 'gi'
2823 ),
2824 '###' + tags[i] + ' class="$2"###'
2825 );
2826 html = html.replace(new RegExp(
2827 '<' + tags[i] + '([^>]*)data-redactor-style-cache="([^>]*)"[^>]*>',
2828 'gi'
2829 ), '###' + tags[i] + ' cache="$2"###');
2830 html = html.replace(new RegExp('<' + tags[i] + '[^>]*>', 'gi'),
2831 '###' + tags[i] + '###'
2832 );
2833 }
2834 else {
2835 html = html.replace(new RegExp('<' + tags[i] + '[^>]*>', 'gi'),
2836 '###' + tags[i] + '###'
2837 );
2838 }
2839 }
2840
2841 return html;
2842
2843 },
2844 reconvertTags: function (html, data) {
2845 // links & images
2846 if ((data.links && this.opts.pasteLinks) || (data.images && this.opts.pasteImages)) {
2847 html = html.replace(new RegExp('#####\\[', 'gi'), '<');
2848 html = html.replace(new RegExp('\\]#####', 'gi'), '>');
2849 }
2850
2851 // plain text
2852 if (this.opts.pastePlainText) {
2853 return html;
2854 }
2855
2856 var blockTags = (data.lists) ? ['ul', 'ol', 'li'] : this.opts.pasteBlockTags;
2857
2858 var tags;
2859 if (data.block || data.lists) {
2860 tags = (data.inline) ? blockTags.concat(this.opts.pasteInlineTags) : blockTags;
2861 }
2862 else {
2863 tags = (data.inline) ? this.opts.pasteInlineTags : [];
2864 }
2865
2866 var len = tags.length;
2867 for (var i = 0; i < len; i++) {
2868 html = html.replace(new RegExp('###\/' + tags[i] + '###', 'gi'),
2869 '</' + tags[i] + '>'
2870 );
2871 }
2872
2873 for (var i = 0; i < len; i++) {
2874 html = html.replace(new RegExp('###' + tags[i] + '###', 'gi'),
2875 '<' + tags[i] + '>'
2876 );
2877 }
2878
2879 for (var i = 0; i < len; i++) {
2880 if (tags[i] === 'td' || tags[i] === 'th') {
2881 html = html.replace(
2882 new RegExp('###' + tags[i] + '\s?(.*?[^#])###', 'gi'),
2883 '<' + tags[i] + '$1>'
2884 );
2885 }
2886 else if (this.utils.isInlineTag(tags[i])) {
2887
2888 var spanMarker = (tags[i] === 'span') ? ' data-redactor-span="true"' : '';
2889
2890 html = html.replace(new RegExp('###' + tags[i] + ' cache="(.*?[^#])"###',
2891 'gi'
2892 ),
2893 '<' + tags[i] + ' style="$1"' + spanMarker + ' data-redactor-style-cache="$1">'
2894 );
2895 html = html.replace(
2896 new RegExp('###' + tags[i] + '\s?(.*?[^#])###', 'gi'),
2897 '<' + tags[i] + '$1>'
2898 );
2899 }
2900 }
2901
2902 return html;
2903
2904 },
2905 cleanPre: function (block) {
2906 block = (typeof block === 'undefined') ? $(this.selection.block()).closest('pre',
2907 this.core.editor()[0]
2908 ) : block;
2909
2910 $(block).find('br').replaceWith(function () {
2911 return document.createTextNode('\n');
2912 });
2913
2914 $(block).find('p').replaceWith(function () {
2915 return $(this).contents();
2916 });
2917
2918 },
2919 removeTagsInsidePre: function (html) {
2920 var $div = $('<div />').append(html);
2921 $div.find('pre').replaceWith(function () {
2922 var str = $(this).html();
2923 str = str.replace(/<br\s?\/?>|<\/p>|<\/div>|<\/li>|<\/td>/gi, '\n');
2924 str = str.replace(/(<([^>]+)>)/gi, '');
2925
2926 return $('<pre />').append(str);
2927 });
2928
2929 html = $div.html();
2930 $div.remove();
2931
2932 return html;
2933
2934 },
2935 getPlainText: function (html) {
2936 html = html.replace(/<!--[\s\S]*?-->/gi, '');
2937 html = html.replace(/<style[\s\S]*?style>/gi, '');
2938 html = html.replace(/<p><\/p>/g, '');
2939 html = html.replace(/<\/div>|<\/li>|<\/td>/gi, '\n');
2940 html = html.replace(/<\/p>/gi, '\n\n');
2941 html = html.replace(/<\/H[1-6]>/gi, '\n\n');
2942
2943 var tmp = document.createElement('div');
2944 tmp.innerHTML = html;
2945 html = tmp.textContent || tmp.innerText;
2946
2947 return $.trim(html);
2948 },
2949 savePreCode: function (html) {
2950 html = this.clean.savePreFormatting(html);
2951 html = this.clean.saveCodeFormatting(html);
2952 html = this.clean.restoreSelectionMarkers(html);
2953
2954 return html;
2955 },
2956 savePreFormatting: function (html) {
2957 var pre = html.match(/<pre(.*?)>([\w\W]*?)<\/pre>/gi);
2958 if (pre === null) {
2959 return html;
2960 }
2961
2962 $.each(pre, $.proxy(function (i, s) {
2963 var arr = [];
2964 var codeTag = false;
2965 var contents, attr1, attr2;
2966
2967 if (s.match(/<pre(.*?)>(([\n\r\s]+)?)<code(.*?)>/i)) {
2968 arr = s.match(
2969 /<pre(.*?)>(([\n\r\s]+)?)<code(.*?)>([\w\W]*?)<\/code>(([\n\r\s]+)?)<\/pre>/i);
2970 codeTag = true;
2971
2972 contents = arr[5];
2973 attr1 = arr[1];
2974 attr2 = arr[4];
2975 }
2976 else {
2977 arr = s.match(/<pre(.*?)>([\w\W]*?)<\/pre>/i);
2978
2979 contents = arr[2];
2980 attr1 = arr[1];
2981 }
2982
2983 contents = contents.replace(/<br\s?\/?>/g, '\n');
2984 contents = contents.replace(/&nbsp;/g, ' ');
2985
2986 if (this.opts.preSpaces) {
2987 contents = contents.replace(/\t/g,
2988 new Array(this.opts.preSpaces + 1).join(' ')
2989 );
2990 }
2991
2992 contents = this.clean.encodeEntities(contents);
2993
2994 // $ fix
2995 contents = contents.replace(/\$/g, '&#36;');
2996
2997 if (codeTag) {
2998 html = html.replace(s,
2999 '<pre' + attr1 + '><code' + attr2 + '>' + contents + '</code></pre>'
3000 );
3001 }
3002 else {
3003 html = html.replace(s,
3004 '<pre' + attr1 + '>' + contents + '</pre>'
3005 );
3006 }
3007
3008 }, this));
3009
3010 return html;
3011 },
3012 saveCodeFormatting: function (html) {
3013 var code = html.match(/<code(.*?)>([\w\W]*?)<\/code>/gi);
3014 if (code === null) {
3015 return html;
3016 }
3017
3018 $.each(code, $.proxy(function (i, s) {
3019 var arr = s.match(/<code(.*?)>([\w\W]*?)<\/code>/i);
3020
3021 arr[2] = arr[2].replace(/&nbsp;/g, ' ');
3022 arr[2] = this.clean.encodeEntities(arr[2]);
3023 arr[2] = arr[2].replace(/\$/g, '&#36;');
3024
3025 html = html.replace(s, '<code' + arr[1] + '>' + arr[2] + '</code>');
3026
3027 }, this));
3028
3029 return html;
3030 },
3031 restoreSelectionMarkers: function (html) {
3032 html = html.replace(
3033 /&lt;span id=&quot;selection-marker-([0-9])&quot; class=&quot;redactor-selection-marker&quot;&gt;​&lt;\/span&gt;/g,
3034 '<span id="selection-marker-$1" class="redactor-selection-marker">​</span>'
3035 );
3036
3037 return html;
3038 },
3039 saveFormTags: function (html) {
3040 return html;
3041 },
3042 restoreFormTags: function (html) {
3043 return html.replace(
3044 /<section(.*?) rel="redactor-form-tag"(.*?)>([\w\W]*?)<\/section>/gi,
3045 '<form$1$2>$3</form>'
3046 );
3047 },
3048 encodeHtml: function (html) {
3049 html = html.replace(/”/g, '"');
3050 html = html.replace(/“/g, '"');
3051 html = html.replace(/‘/g, '\'');
3052 html = html.replace(/’/g, '\'');
3053 html = this.clean.encodeEntities(html);
3054
3055 return html;
3056 },
3057 encodeEntities: function (str) {
3058 str = String(str).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g,
3059 '>'
3060 ).replace(/&quot;/g, '"');
3061 str = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g,
3062 '&gt;'
3063 ).replace(/"/g, '&quot;');
3064
3065 return str;
3066 },
3067 stripTags: function (input, denied) {
3068 if (typeof denied === 'undefined') {
3069 return input.replace(/(<([^>]+)>)/gi, '');
3070 }
3071
3072 var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
3073
3074 return input.replace(tags, function ($0, $1) {
3075 return denied.indexOf($1.toLowerCase()) === -1 ? $0 : '';
3076 });
3077 },
3078 removeMarkers: function (html) {
3079 return html.replace(
3080 /<span(.*?[^>]?)class="redactor-selection-marker"(.*?[^>]?)>([\w\W]*?)<\/span>/gi,
3081 ''
3082 );
3083 },
3084 removeSpaces: function (html) {
3085 html = $.trim(html);
3086 html = html.replace(/\n/g, '');
3087 html = html.replace(/[\t]*/g, '');
3088 html = html.replace(/\n\s*\n/g, '\n');
3089 html = html.replace(/^[\s\n]*/g, ' ');
3090 html = html.replace(/[\s\n]*$/g, ' ');
3091 html = html.replace(/>\s{2,}</g, '> <'); // between inline tags can be only one space
3092 html = html.replace(/\n\n/g, '\n');
3093 html = html.replace(/\u200B/g, '');
3094
3095 return html;
3096 },
3097 removeSpacesHard: function (html) {
3098 html = $.trim(html);
3099 html = html.replace(/\n/g, '');
3100 html = html.replace(/[\t]*/g, '');
3101 html = html.replace(/\n\s*\n/g, '\n');
3102 html = html.replace(/^[\s\n]*/g, '');
3103 html = html.replace(/[\s\n]*$/g, '');
3104 html = html.replace(/>\s{2,}</g, '><');
3105 html = html.replace(/\n\n/g, '\n');
3106 html = html.replace(/\u200B/g, '');
3107
3108 return html;
3109 },
3110 normalizeCurrentHeading: function () {
3111 var heading = this.selection.block();
3112 if (this.utils.isCurrentOrParentHeader() && heading) {
3113 heading.normalize();
3114 }
3115 }
3116 };
3117 },
3118
3119 // =code
3120 code: function () {
3121 return {
3122 syncFire: true,
3123 html: false,
3124 start: function (html) {
3125 html = $.trim(html);
3126 html = html.replace(
3127 /^(<span id="selection-marker-1" class="redactor-selection-marker">​<\/span>)/,
3128 ''
3129 );
3130
3131 html = this.clean.onSet(html);
3132
3133 html = html.replace(
3134 /<p><span id="selection-marker-1" class="redactor-selection-marker">​<\/span><\/p>/,
3135 ''
3136 );
3137
3138 this.events.stopDetectChanges();
3139 this.core.editor().html(html);
3140
3141 this.observe.load();
3142 this.events.startDetectChanges();
3143 },
3144 set: function (html, options) {
3145 html = $.trim(html);
3146
3147 options = options || {};
3148
3149 // start
3150 if (options.start) {
3151 this.start = options.start;
3152 }
3153
3154 // clean
3155 if (this.opts.type === 'textarea') {
3156 html = this.clean.onSet(html);
3157 }
3158 else if (this.opts.type === 'div' && html === '') {
3159 html = this.opts.emptyHtml;
3160 }
3161
3162 this.core.editor().html(html);
3163
3164 if (this.opts.type === 'textarea') {
3165 this.code.sync();
3166 }
3167 },
3168 get: function () {
3169 if (this.opts.type === 'textarea') {
3170 return this.core.textarea().val();
3171 }
3172 else {
3173 var html = this.core.editor().html();
3174
3175 // clean
3176 html = this.clean.onGet(html);
3177
3178 return html;
3179 }
3180 },
3181 sync: function () {
3182 if (!this.code.syncFire) {
3183 return;
3184 }
3185
3186 var html = this.core.editor().html();
3187 var htmlCleaned = this.code.cleaned(html);
3188
3189 // is there a need to synchronize
3190 if (this.code.isSync(htmlCleaned)) {
3191 // do not sync
3192 return;
3193 }
3194
3195 // save code
3196 this.code.html = htmlCleaned;
3197
3198 if (this.opts.type !== 'textarea') {
3199 this.core.callback('sync', html);
3200 this.core.callback('change', html);
3201 return;
3202 }
3203
3204 if (this.opts.type === 'textarea') {
3205 setTimeout($.proxy(function () {
3206 this.code.startSync(html);
3207
3208 }, this), 10);
3209 }
3210 },
3211 startSync: function (html) {
3212 // before clean callback
3213 html = this.core.callback('syncBefore', html);
3214
3215 // clean
3216 html = this.clean.onSync(html);
3217
3218 // set code
3219 this.core.textarea().val(html);
3220
3221 // after sync callback
3222 this.core.callback('sync', html);
3223
3224 // change callback
3225 if (this.start === false) {
3226 this.core.callback('change', html);
3227 }
3228
3229 this.start = false;
3230 },
3231 isSync: function (htmlCleaned) {
3232 var html = (this.code.html !== false) ? this.code.html : false;
3233
3234 return (html !== false && html === htmlCleaned);
3235 },
3236 cleaned: function (html) {
3237 html = html.replace(/\u200B/g, '');
3238 return this.clean.removeMarkers(html);
3239 }
3240 };
3241 },
3242
3243 // =core
3244 core: function () {
3245 return {
3246
3247 id: function () {
3248 return this.$editor.attr('id');
3249 },
3250 element: function () {
3251 return this.$element;
3252 },
3253 editor: function () {
3254 return (typeof this.$editor === 'undefined') ? $() : this.$editor;
3255 },
3256 textarea: function () {
3257 return this.$textarea;
3258 },
3259 box: function () {
3260 return (this.opts.type === 'textarea') ? this.$box : this.$element;
3261 },
3262 toolbar: function () {
3263 return (this.$toolbar) ? this.$toolbar : false;
3264 },
3265 air: function () {
3266 return (this.$air) ? this.$air : false;
3267 },
3268 object: function () {
3269 return $.extend({}, this);
3270 },
3271 structure: function () {
3272 this.core.editor().toggleClass('redactor-structure');
3273 },
3274 addEvent: function (name) {
3275 this.core.event = name;
3276 },
3277 getEvent: function () {
3278 return this.core.event;
3279 },
3280 callback: function (type, e, data) {
3281 var eventNamespace = 'redactor';
3282 var returnValue = false;
3283 var events = $._data(this.core.element()[0], 'events');
3284
3285 // on callback
3286 if (typeof events !== 'undefined' && typeof events[type] !== 'undefined') {
3287 var len = events[type].length;
3288 for (var i = 0; i < len; i++) {
3289 var namespace = events[type][i].namespace;
3290 if (namespace === 'callback.' + eventNamespace) {
3291 var handler = events[type][i].handler;
3292 var args = (typeof data === 'undefined') ? [e] : [
3293 e, data
3294 ];
3295 returnValue = (typeof args === 'undefined') ? handler.call(this,
3296 e
3297 ) : handler.call(this, e, args);
3298 }
3299 }
3300 }
3301
3302 if (returnValue) {
3303 return returnValue;
3304 }
3305
3306 // no callback
3307 if (typeof this.opts.callbacks[type] === 'undefined') {
3308 return (typeof data === 'undefined') ? e : data;
3309 }
3310
3311 // callback
3312 var callback = this.opts.callbacks[type];
3313
3314 if ($.isFunction(callback)) {
3315 return (typeof data === 'undefined') ? callback.call(this,
3316 e
3317 ) : callback.call(this, e, data);
3318 }
3319 else {
3320 return (typeof data === 'undefined') ? e : data;
3321 }
3322 },
3323 destroy: function () {
3324 this.opts.destroyed = true;
3325
3326 this.core.callback('destroy');
3327
3328 // help label
3329 $('#redactor-voice-' + this.uuid).remove();
3330
3331 this.core.editor().removeClass(
3332 'redactor-in redactor-styles redactor-structure redactor-layer-img-edit');
3333
3334 // caret service
3335 this.core.editor().off('keydown.redactor-remove-textnode');
3336
3337 // observer
3338 this.core.editor().off('.redactor-observe.' + this.uuid);
3339
3340 // off events and remove data
3341 this.$element.off('.redactor').removeData('redactor');
3342 this.core.editor().off('.redactor');
3343
3344 $(document).off('.redactor-air.' + this.uuid);
3345 $(document).off('mousedown.redactor-blur.' + this.uuid);
3346 $(document).off('mousedown.redactor.' + this.uuid);
3347 $(document).off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid);
3348 $(window).off('.redactor-toolbar.' + this.uuid);
3349 $(window).off('touchmove.redactor.' + this.uuid);
3350 $('body').off('scroll.redactor.' + this.uuid);
3351
3352 $(this.opts.toolbarFixedTarget).off('scroll.redactor.' + this.uuid);
3353
3354 // plugins events
3355 var self = this;
3356 if (this.opts.plugins !== false) {
3357 $.each(this.opts.plugins, function (i, s) {
3358 $(window).off('.redactor-plugin-' + s);
3359 $(document).off('.redactor-plugin-' + s);
3360 $('body').off('.redactor-plugin-' + s);
3361 self.core.editor().off('.redactor-plugin-' + s);
3362 });
3363 }
3364
3365 // click to edit
3366 this.$element.off('click.redactor-click-to-edit');
3367 this.$element.removeClass('redactor-click-to-edit');
3368
3369 // common
3370 this.core.editor().removeClass('redactor-layer');
3371 this.core.editor().removeAttr('contenteditable');
3372
3373 var html = this.code.get();
3374
3375 if (this.opts.toolbar && this.$toolbar) {
3376 // dropdowns off
3377 this.$toolbar.find('a').each(function () {
3378 var $el = $(this);
3379 if ($el.data('dropdown')) {
3380 $el.data('dropdown').remove();
3381 $el.data('dropdown', {});
3382 }
3383 });
3384 }
3385
3386 if (this.opts.type === 'textarea') {
3387 this.$box.after(this.$element);
3388 this.$box.remove();
3389 this.$element.val(html).show();
3390 }
3391
3392 if (this.opts.toolbar && this.$toolbar) {
3393 this.$toolbar.remove();
3394 }
3395
3396 // modal
3397 if (this.$modalBox) {
3398 this.$modalBox.remove();
3399 }
3400
3401 if (this.$modalOverlay) {
3402 this.$modalOverlay.remove();
3403 }
3404
3405 // hide link's tooltip
3406 $('.redactor-link-tooltip').remove();
3407 }
3408 };
3409 },
3410
3411 // =detect
3412 detect: function () {
3413 return {
3414
3415 // public
3416 isWebkit: function () {
3417 return /webkit/.test(this.opts.userAgent);
3418 },
3419 isFirefox: function () {
3420 return this.opts.userAgent.indexOf('firefox') > -1;
3421 },
3422 isIe: function (v) {
3423 if (document.documentMode || /Edge/.test(navigator.userAgent)) {
3424 return 'edge';
3425 }
3426
3427 var ie;
3428 ie = RegExp('msie' + (!isNaN(v) ? ('\\s' + v) : ''),
3429 'i'
3430 ).test(navigator.userAgent);
3431
3432 if (!ie) {
3433 ie = !!navigator.userAgent.match(/Trident.*rv[ :]*11\./);
3434 }
3435
3436 return ie;
3437 },
3438 isMobile: function () {
3439 return /(iPhone|iPod|BlackBerry|Android)/.test(navigator.userAgent);
3440 },
3441 isDesktop: function () {
3442 return !/(iPhone|iPod|iPad|BlackBerry|Android)/.test(navigator.userAgent);
3443 },
3444 isIpad: function () {
3445 return /iPad/.test(navigator.userAgent);
3446 }
3447
3448 };
3449 },
3450
3451 // =dropdown
3452 dropdown: function () {
3453 return {
3454 active: false,
3455 button: false,
3456 key: false,
3457 position: [],
3458 getDropdown: function () {
3459 return this.dropdown.active;
3460 },
3461 build: function (name, $dropdown, dropdownObject) {
3462 var btnObject, fragment = document.createDocumentFragment();
3463 for (var btnName in dropdownObject) {
3464 if (dropdownObject.hasOwnProperty(btnName)) {
3465 btnObject = dropdownObject[btnName];
3466
3467 var item = this.dropdown.buildItem(btnName, btnObject);
3468
3469 this.observe.addDropdown($(item), btnName, btnObject);
3470 fragment.appendChild(item);
3471 }
3472 }
3473
3474 var hasItems = false;
3475 for (var i = 0, length = fragment.childNodes.length; i < length; i++) {
3476 if (fragment.childNodes[i].nodeType === Node.ELEMENT_NODE) {
3477 hasItems = true;
3478 break;
3479 }
3480 }
3481
3482 if (hasItems) {
3483 $dropdown[0].rel = name;
3484 $dropdown[0].appendChild(fragment);
3485 }
3486 },
3487 buildFormatting: function () {},
3488 buildItem: function (btnName, btnObject) {
3489 var itemContainer = elCreate('li');
3490 if (typeof btnObject.classname !== 'undefined') {
3491 itemContainer.classList.add(btnObject.classname);
3492 }
3493
3494 if (btnName.toLowerCase().indexOf('divider') === 0) {
3495 itemContainer.classList.add('redactor-dropdown-divider');
3496
3497 return itemContainer;
3498 }
3499
3500 itemContainer.innerHTML = '<a href="#" class="redactor-dropdown-' + btnName + '" role="button"><span>' + btnObject.title + '</span></a>';
3501 // Use a jQuery event here to support the unbinding of the event listener in
3502 // existing "3rdParty" code.
3503 $(itemContainer.children[0]).on('mousedown', (function(event) {
3504 event.preventDefault();
3505
3506 this.dropdown.buildClick(event, btnName, btnObject);
3507 }).bind(this));
3508
3509 return itemContainer;
3510 },
3511 buildClick: function (e, btnName, btnObject) {
3512 if ($(e.target).hasClass('redactor-dropdown-link-inactive')) {
3513 return;
3514 }
3515
3516 var command = this.dropdown.buildCommand(btnObject);
3517
3518 if (typeof btnObject.args !== 'undefined') {
3519 this.button.toggle(e,
3520 btnName,
3521 command.type,
3522 command.callback,
3523 btnObject.args
3524 );
3525 }
3526 else {
3527 this.button.toggle(e, btnName, command.type, command.callback);
3528 }
3529 },
3530 buildCommand: function (btnObject) {
3531 var command = {};
3532 command.type = 'func';
3533 command.callback = btnObject.func;
3534
3535 if (btnObject.command) {
3536 command.type = 'command';
3537 command.callback = btnObject.command;
3538 }
3539 else if (btnObject.dropdown) {
3540 command.type = 'dropdown';
3541 command.callback = btnObject.dropdown;
3542 }
3543
3544 return command;
3545 },
3546 show: function (e, key) {
3547 if (this.detect.isDesktop()) {
3548 this.core.editor().focus();
3549 }
3550
3551 this.dropdown.hideAll(false, key);
3552
3553 this.dropdown.key = key;
3554 this.dropdown.button = this.button.get(this.dropdown.key);
3555
3556 require(['Ui/SimpleDropdown'], (function(UiSimpleDropdown) {
3557 var dropdownId = this.dropdown.button[0].id;
3558
3559 UiSimpleDropdown.toggleDropdown(dropdownId);
3560 if (UiSimpleDropdown.isOpen(dropdownId)) {
3561 this.dropdown.active = $(UiSimpleDropdown.getDropdownMenu(dropdownId));
3562
3563 this.core.callback('dropdownShow', {
3564 dropdown: this.dropdown.active,
3565 key: this.dropdown.key,
3566 button: this.dropdown.button
3567 });
3568
3569 this.button.setActive(this.dropdown.key);
3570 this.dropdown.button.addClass('dropact').attr('aria-expanded', true);
3571
3572 this.dropdown.enableCallback();
3573 }
3574 else {
3575 this.dropdown.hide();
3576 }
3577 }).bind(this));
3578
3579 e.preventDefault();
3580 },
3581 showIsFixedToolbar: function () {},
3582 showIsUnFixedToolbar: function () {},
3583 enableEvents: function () {},
3584 enableCallback: function () {
3585 this.core.callback('dropdownShown', {
3586 dropdown: this.dropdown.active,
3587 key: this.dropdown.key,
3588 button: this.dropdown.button
3589 });
3590 },
3591 getButtonPosition: function () {},
3592 closeHandler: function () {},
3593 hideAll: function (e, key) { this.dropdown.hideOut(key); },
3594 hide: function () { this.dropdown.hideOut(); },
3595 hideOut: function (key) {
3596 if (this.dropdown.active === false) {
3597 return;
3598 }
3599
3600 if (this.dropdown.button[0].rel === key) {
3601 return;
3602 }
3603
3604 this.core.callback('dropdownHide', this.dropdown.active);
3605
3606 var id = this.dropdown.button[0].id;
3607 require(['Ui/SimpleDropdown'], function(UiSimpleDropdown) {
3608 UiSimpleDropdown.close(id);
3609 });
3610
3611 this.dropdown.button.removeClass('redactor-act dropact').attr('aria-expanded', false);
3612 this.dropdown.button = false;
3613 this.dropdown.key = false;
3614 this.dropdown.active = false;
3615 }
3616 };
3617 },
3618
3619 // =events
3620 events: function () {
3621 return {
3622 focused: false,
3623 blured: true,
3624 dropImage: false,
3625 stopChanges: false,
3626 stopDetectChanges: function () {
3627 this.events.stopChanges = true;
3628 },
3629 startDetectChanges: function () {
3630 var self = this;
3631 setTimeout(function () {
3632 self.events.stopChanges = false;
3633 }, 1);
3634 },
3635 dragover: function (e) {
3636 e.preventDefault();
3637
3638 if (e.target.tagName === 'IMG') {
3639 $(e.target).addClass('redactor-image-dragover');
3640 }
3641
3642 },
3643 dragleave: function (e) {
3644 // remove image dragover
3645 this.core.editor().find('img').removeClass('redactor-image-dragover');
3646 },
3647 drop: function (e) {
3648 e = e.originalEvent || e;
3649
3650 // remove image dragover
3651 this.core.editor().find('img').removeClass('redactor-image-dragover');
3652
3653 if (this.opts.type === 'inline' || this.opts.type === 'pre') {
3654 e.preventDefault();
3655 return false;
3656 }
3657
3658 if (window.FormData === undefined || !e.dataTransfer) {
3659 return true;
3660 }
3661
3662 if (e.dataTransfer.files.length === 0) {
3663 return this.events.onDrop(e);
3664 }
3665 else {
3666 this.events.onDropUpload(e);
3667 }
3668
3669 this.core.callback('drop', e);
3670
3671 },
3672 click: function (e) {
3673 var event = this.core.getEvent();
3674 var type = (event === 'click' || event === 'arrow') ? false : 'click';
3675
3676 this.core.addEvent(type);
3677 this.utils.disableSelectAll();
3678 this.core.callback('click', e);
3679 },
3680 focus: function (e) {
3681 if (this.rtePaste) {
3682 return;
3683 }
3684
3685 if (this.events.isCallback('focus')) {
3686 this.core.callback('focus', e);
3687 }
3688
3689 this.events.focused = true;
3690 this.events.blured = false;
3691
3692 // tab
3693 if (this.selection.current() === false) {
3694 var sel = this.selection.get();
3695 var range = this.selection.range(sel);
3696
3697 range.setStart(this.core.editor()[0], 0);
3698 range.setEnd(this.core.editor()[0], 0);
3699 this.selection.update(sel, range);
3700 }
3701
3702 },
3703 blur: function (e) {
3704 if (this.start || this.rtePaste) {
3705 return;
3706 }
3707
3708 if ($(e.target).closest('#' + this.core.id() + ', .redactor-toolbar, .redactor-dropdown, #redactor-modal-box').length !== 0) {
3709 return;
3710 }
3711
3712 if (!this.events.blured && this.events.isCallback('blur')) {
3713 this.core.callback('blur', e);
3714 }
3715
3716 this.events.focused = false;
3717 this.events.blured = true;
3718 },
3719 touchImageEditing: function () {
3720 var scrollTimer = -1;
3721 this.events.imageEditing = false;
3722 $(window).on('touchmove.redactor.' + this.uuid, $.proxy(function () {
3723 this.events.imageEditing = true;
3724 if (scrollTimer !== -1) {
3725 clearTimeout(scrollTimer);
3726 }
3727
3728 scrollTimer = setTimeout($.proxy(function () {
3729 this.events.imageEditing = false;
3730
3731 }, this), 500);
3732
3733 }, this));
3734 },
3735 init: function () {
3736 this.core.editor().on('dragover.redactor dragenter.redactor',
3737 $.proxy(this.events.dragover, this)
3738 );
3739 this.core.editor().on('dragleave.redactor',
3740 $.proxy(this.events.dragleave, this)
3741 );
3742 this.core.editor().on('drop.redactor', $.proxy(this.events.drop, this));
3743 this.core.editor().on('click.redactor', $.proxy(this.events.click, this));
3744 this.core.editor().on('paste.redactor', $.proxy(this.paste.init, this));
3745 this.core.editor().on('keydown.redactor', $.proxy(this.keydown.init, this));
3746 this.core.editor().on('keyup.redactor', $.proxy(this.keyup.init, this));
3747 this.core.editor().on('focus.redactor', $.proxy(this.events.focus, this));
3748
3749 $(document).on('mousedown.redactor-blur.' + this.uuid,
3750 $.proxy(this.events.blur, this)
3751 );
3752
3753 this.events.touchImageEditing();
3754
3755 this.events.createObserver();
3756 this.events.setupObserver();
3757
3758 },
3759 createObserver: function () {
3760 var self = this;
3761 this.events.observer = new MutationObserver(function (mutations) {
3762 mutations.forEach($.proxy(self.events.iterateObserver, self));
3763 });
3764
3765 },
3766 iterateObserver: function (mutation) {
3767
3768 var stop = false;
3769
3770 // target
3771 if (((this.opts.type === 'textarea' || this.opts.type === 'div') && (!this.detect.isFirefox() && mutation.target === this.core.editor()[0])) || (mutation.attributeName === 'class' && mutation.target === this.core.editor()[0]) || (mutation.attributeName == 'data-vivaldi-spatnav-clickable')) {
3772 stop = true;
3773 }
3774
3775 if (!stop) {
3776 this.observe.load();
3777 this.events.changeHandler();
3778 }
3779 },
3780 setupObserver: function () {
3781 this.events.observer.observe(this.core.editor()[0], {
3782 attributes: true,
3783 subtree: true,
3784 childList: true,
3785 characterData: true,
3786 characterDataOldValue: true
3787 });
3788 },
3789 changeHandler: function () {
3790 if (this.events.stopChanges) {
3791 return;
3792 }
3793
3794 this.code.sync();
3795
3796 },
3797 onDropUpload: function (e) {
3798 e.preventDefault();
3799 e.stopPropagation();
3800
3801 if ((!this.opts.dragImageUpload && !this.opts.dragFileUpload) || (this.opts.imageUpload === null && this.opts.fileUpload === null)) {
3802 return;
3803 }
3804
3805 if (e.target.tagName === 'IMG') {
3806 this.events.dropImage = e.target;
3807 }
3808
3809 var files = e.dataTransfer.files;
3810 var len = files.length;
3811 for (var i = 0; i < len; i++) {
3812 this.upload.directUpload(files[i], e);
3813 }
3814 },
3815 onDrop: function (e) {
3816 this.core.callback('drop', e);
3817 },
3818 isCallback: function (name) {
3819 return (typeof this.opts.callbacks[name] !== 'undefined' && $.isFunction(this.opts.callbacks[name]));
3820 },
3821
3822 // #backward
3823 stopDetect: function () {
3824 this.events.stopDetectChanges();
3825 },
3826 startDetect: function () {
3827 this.events.startDetectChanges();
3828 }
3829
3830 };
3831 },
3832
3833 // =file -- UNSUPPORTED MODULE
3834 file: function () {
3835 return {
3836 is: function () {},
3837 show: function () {},
3838 insert: function () {},
3839 release: function () {},
3840 text: function (json) {}
3841 };
3842 },
3843
3844 // =focus
3845 focus: function () {
3846 return {
3847 start: function () {
3848 this.core.editor().focus();
3849
3850 if (this.opts.type === 'inline') {
3851 return;
3852 }
3853
3854 var $first = this.focus.first();
3855 if ($first !== false) {
3856 this.caret.start($first);
3857 }
3858 },
3859 end: function () {
3860 this.core.editor().focus();
3861
3862 var last = (this.opts.inline) ? this.core.editor() : this.focus.last();
3863 if (last.length === 0) {
3864 return;
3865 }
3866
3867 // get inline last node
3868 var lastNode = this.focus.lastChild(last);
3869 if (!this.detect.isWebkit() && lastNode !== false) {
3870 this.caret.end(lastNode);
3871 }
3872 else {
3873 var sel = this.selection.get();
3874 var range = this.selection.range(sel);
3875
3876 if (range !== null) {
3877 range.selectNodeContents(last[0]);
3878 range.collapse(false);
3879
3880 this.selection.update(sel, range);
3881 }
3882 else {
3883 this.caret.end(last);
3884 }
3885 }
3886
3887 },
3888 first: function () {
3889 var $first = this.core.editor().children().first();
3890 if ($first.length === 0 && ($first[0].length === 0 || $first[0].tagName === 'BR' || $first[0].tagName === 'HR' || $first[0].nodeType === 3)) {
3891 return false;
3892 }
3893
3894 if ($first[0].tagName === 'UL' || $first[0].tagName === 'OL') {
3895 return $first.find('li').first();
3896 }
3897
3898 return $first;
3899
3900 },
3901 last: function () {
3902 return this.core.editor().children().last();
3903 },
3904 lastChild: function (last) {
3905 var lastNode = last[0].lastChild;
3906
3907 return (lastNode !== null && this.utils.isInlineTag(lastNode.tagName)) ? lastNode : false;
3908 },
3909 is: function () {
3910 return (this.core.editor()[0] === document.activeElement);
3911 }
3912 };
3913 },
3914
3915 // =image
3916 image: function () {
3917 return {
3918 is: function () {
3919 return !(!this.opts.imageUpload || !this.opts.imageUpload && !this.opts.s3);
3920 },
3921 show: function () {
3922 // build modal
3923 this.modal.load('image', this.lang.get('image'), 700);
3924
3925 // build upload
3926 this.upload.init('#redactor-modal-image-droparea',
3927 this.opts.imageUpload,
3928 this.image.insert
3929 );
3930 this.modal.show();
3931
3932 },
3933 insert: function (json, direct, e) {
3934 var $img;
3935
3936 // error callback
3937 if (typeof json.error !== 'undefined') {
3938 this.modal.close();
3939 this.events.dropImage = false;
3940 this.core.callback('imageUploadError', json, e);
3941 return;
3942 }
3943
3944 // change image
3945 if (this.events.dropImage !== false) {
3946 $img = $(this.events.dropImage);
3947
3948 this.core.callback('imageDelete', $img[0].src, $img);
3949
3950 $img.attr('src', json.url);
3951
3952 this.events.dropImage = false;
3953 this.core.callback('imageUpload', $img, json);
3954 return;
3955 }
3956
3957 var $figure = $('<' + this.opts.imageTag + '>');
3958
3959 $img = $('<img>');
3960 $img.attr('src', json.url);
3961
3962 // set id
3963 var id = (typeof json.id === 'undefined') ? '' : json.id;
3964 var type = (typeof json.s3 === 'undefined') ? 'image' : 's3';
3965 $img.attr('data-' + type, id);
3966
3967 $figure.append($img);
3968
3969 var pre = this.utils.isTag(this.selection.current(), 'pre');
3970
3971 if (direct) {
3972 this.marker.remove();
3973
3974 var node = this.insert.nodeToPoint(e, this.marker.get());
3975 var $next = $(node).next();
3976
3977 this.selection.restore();
3978
3979 // buffer
3980 this.buffer.set();
3981
3982 // insert
3983 if (typeof $next !== 'undefined' && $next.length !== 0 && $next[0].tagName === 'IMG') {
3984 // delete callback
3985 this.core.callback('imageDelete', $next[0].src, $next);
3986
3987 // replace
3988 $next.closest('figure, p', this.core.editor()[0]).replaceWith(
3989 $figure);
3990 this.caret.after($figure);
3991 }
3992 else {
3993 if (pre) {
3994 $(pre).after($figure);
3995 }
3996 else {
3997 this.insert.node($figure);
3998 }
3999
4000 this.caret.after($figure);
4001 }
4002
4003 }
4004 else {
4005 this.modal.close();
4006
4007 // buffer
4008 this.buffer.set();
4009
4010 // insert
4011 if (pre) {
4012 $(pre).after($figure);
4013 }
4014 else {
4015 this.insert.node($figure);
4016 }
4017
4018 this.caret.after($figure);
4019 }
4020
4021 this.events.dropImage = false;
4022
4023 var nextNode = $img[0].nextSibling;
4024 var $nextFigure = $figure.next();
4025 var isNextEmpty = $(nextNode).text().replace(/\u200B/g, '');
4026 var isNextFigureEmpty = $nextFigure.text().replace(/\u200B/g, '');
4027
4028 if (isNextEmpty === '') {
4029 $(nextNode).remove();
4030 }
4031
4032 if ($nextFigure.length === 1 && $nextFigure[0].tagName === 'FIGURE' && isNextFigureEmpty === '') {
4033 $nextFigure.remove();
4034 }
4035
4036 if (direct !== null) {
4037 this.core.callback('imageUpload', $img, json);
4038 }
4039 else {
4040 this.core.callback('imageInserted', $img, json);
4041 }
4042 },
4043 setEditable: function ($image) {
4044 $image.on('dragstart', function (e) {
4045 e.preventDefault();
4046 });
4047
4048 if (this.opts.imageResizable) {
4049 var handler = $.proxy(function (e) {
4050 this.observe.image = $image;
4051 this.image.resizer = this.image.loadEditableControls($image);
4052
4053 $(document).on('mousedown.redactor-image-resize-hide.' + this.uuid,
4054 $.proxy(this.image.hideResize, this)
4055 );
4056
4057 if (this.image.resizer) {
4058 this.image.resizer.on(
4059 'mousedown.redactor touchstart.redactor',
4060 $.proxy(function (e) {
4061 this.image.setResizable(e, $image);
4062 }, this)
4063 );
4064 }
4065
4066 }, this);
4067
4068 $image.off('mousedown.redactor').on('mousedown.redactor',
4069 $.proxy(this.image.hideResize, this)
4070 );
4071 $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor',
4072 handler
4073 );
4074 }
4075 else {
4076 $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor',
4077 $.proxy(function (e) {
4078 setTimeout($.proxy(function () {
4079 this.image.showEdit($image);
4080
4081 }, this), 200);
4082
4083 }, this)
4084 );
4085 }
4086
4087 },
4088 setResizable: function (e, $image) {
4089 e.preventDefault();
4090
4091 this.image.resizeHandle = {
4092 x: e.pageX,
4093 y: e.pageY,
4094 el: $image,
4095 ratio: $image.width() / $image.height(),
4096 h: $image.height()
4097 };
4098
4099 e = e.originalEvent || e;
4100
4101 if (e.targetTouches) {
4102 this.image.resizeHandle.x = e.targetTouches[0].pageX;
4103 this.image.resizeHandle.y = e.targetTouches[0].pageY;
4104 }
4105
4106 this.image.startResize();
4107 },
4108 startResize: function () {
4109 $(document).on('mousemove.redactor-image-resize touchmove.redactor-image-resize',
4110 $.proxy(this.image.moveResize, this)
4111 );
4112 $(document).on('mouseup.redactor-image-resize touchend.redactor-image-resize',
4113 $.proxy(this.image.stopResize, this)
4114 );
4115 },
4116 moveResize: function (e) {
4117 e.preventDefault();
4118
4119 e = e.originalEvent || e;
4120
4121 var height = this.image.resizeHandle.h;
4122
4123 if (e.targetTouches) height += (e.targetTouches[0].pageY - this.image.resizeHandle.y); else height += (e.pageY - this.image.resizeHandle.y);
4124
4125 var width = Math.round(height * this.image.resizeHandle.ratio);
4126
4127 if (height < 50 || width < 100) return;
4128 if (this.core.editor().width() <= width) return;
4129
4130 this.image.resizeHandle.el.attr({
4131 width: width,
4132 height: height
4133 });
4134 this.image.resizeHandle.el.width(width);
4135 this.image.resizeHandle.el.height(height);
4136
4137 this.code.sync();
4138 },
4139 stopResize: function () {
4140 this.handle = false;
4141 $(document).off('.redactor-image-resize');
4142
4143 this.image.hideResize();
4144 },
4145 hideResize: function (e) {
4146 if (e && $(e.target).closest('#redactor-image-box',
4147 this.$editor[0]
4148 ).length !== 0) {
4149 return;
4150 }
4151 if (e && e.target.tagName == 'IMG') {
4152 var $image = $(e.target);
4153 }
4154
4155 var imageBox = this.$editor.find('#redactor-image-box');
4156 if (imageBox.length === 0) return;
4157
4158 $('#redactor-image-editter').remove();
4159 $('#redactor-image-resizer').remove();
4160
4161 imageBox.find('img').css({
4162 marginTop: imageBox[0].style.marginTop,
4163 marginBottom: imageBox[0].style.marginBottom,
4164 marginLeft: imageBox[0].style.marginLeft,
4165 marginRight: imageBox[0].style.marginRight
4166 });
4167
4168 imageBox.css('margin', '');
4169 imageBox.find('img').css('opacity', '');
4170 imageBox.replaceWith(function () {
4171 return $(this).contents();
4172 });
4173
4174 $(document).off('mousedown.redactor-image-resize-hide.' + this.uuid);
4175
4176 if (typeof this.image.resizeHandle !== 'undefined') {
4177 this.image.resizeHandle.el.attr('rel',
4178 this.image.resizeHandle.el.attr('style')
4179 );
4180 }
4181 },
4182 loadResizableControls: function ($image, imageBox) {
4183 if (this.opts.imageResizable && !this.detect.isMobile()) {
4184 var imageResizer = $(
4185 '<span id="redactor-image-resizer" data-redactor="verified"></span>');
4186
4187 if (!this.detect.isDesktop()) {
4188 imageResizer.css({
4189 width: '15px',
4190 height: '15px'
4191 });
4192 }
4193
4194 imageResizer.attr('contenteditable', false);
4195 imageBox.append(imageResizer);
4196 imageBox.append($image);
4197
4198 return imageResizer;
4199 }
4200 else {
4201 imageBox.append($image);
4202 return false;
4203 }
4204 },
4205 loadEditableControls: function ($image) {
4206 if ($('#redactor-image-box').length !== 0) {
4207 return;
4208 }
4209
4210 var imageBox = $('<span id="redactor-image-box" data-redactor="verified">');
4211 imageBox.css('float', $image.css('float')).attr('contenteditable', false);
4212
4213 if ($image[0].style.margin != 'auto') {
4214 imageBox.css({
4215 marginTop: $image[0].style.marginTop,
4216 marginBottom: $image[0].style.marginBottom,
4217 marginLeft: $image[0].style.marginLeft,
4218 marginRight: $image[0].style.marginRight
4219 });
4220
4221 $image.css('margin', '');
4222 }
4223 else {
4224 imageBox.css({
4225 'display': 'block',
4226 'margin': 'auto'
4227 });
4228 }
4229
4230 $image.css('opacity', '.5').after(imageBox);
4231
4232 if (this.opts.imageEditable) {
4233 // editter
4234 this.image.editter = $(
4235 '<span id="redactor-image-editter" data-redactor="verified">' + this.lang.get(
4236 'edit') + '</span>');
4237 this.image.editter.attr('contenteditable', false);
4238 this.image.editter.on('click', $.proxy(function () {
4239 this.image.showEdit($image);
4240 }, this));
4241
4242 imageBox.append(this.image.editter);
4243
4244 // position correction
4245 var editerWidth = this.image.editter.innerWidth();
4246 this.image.editter.css('margin-left', '-' + editerWidth / 2 + 'px');
4247 }
4248
4249 return this.image.loadResizableControls($image, imageBox);
4250
4251 },
4252 showEdit: function ($image) {
4253 if (this.events.imageEditing) {
4254 return;
4255 }
4256
4257 this.observe.image = $image;
4258
4259 var $link = $image.closest('a', this.$editor[0]);
4260 var $figure = $image.closest('figure', this.$editor[0]);
4261 var $container = ($figure.length !== 0) ? $figure : $image;
4262
4263 this.modal.load('image-edit', this.lang.get('edit'), 705);
4264
4265 this.image.buttonDelete = this.modal.getDeleteButton().text(this.lang.get(
4266 'delete'));
4267 this.image.buttonSave = this.modal.getActionButton().text(this.lang.get('save'));
4268
4269 this.image.buttonDelete.on('click', $.proxy(this.image.remove, this));
4270 this.image.buttonSave.on('click', $.proxy(this.image.update, this));
4271
4272 if (this.opts.imageCaption === false) {
4273 $('#redactor-image-caption').val('').hide().prev().hide();
4274 }
4275 else {
4276 var $parent = $image.closest(this.opts.imageTag, this.$editor[0]);
4277 var $ficaption = $parent.find('figcaption');
4278 if ($ficaption !== 0) {
4279
4280 $('#redactor-image-caption').val($ficaption.text()).show();
4281 }
4282 }
4283
4284 if (!this.opts.imagePosition) {
4285 $('.redactor-image-position-option').hide();
4286 }
4287 else {
4288 var isCentered = ($figure.length !== 0) ? ($container.css('text-align') === 'center') : ($container.css(
4289 'display') == 'block' && $container.css('float') == 'none');
4290 var floatValue = (isCentered) ? 'center' : $container.css('float');
4291 $('#redactor-image-align').val(floatValue);
4292 }
4293
4294 $('#redactor-image-preview').html($('<img src="' + $image.attr('src') + '" style="max-width: 100%;">'));
4295 $('#redactor-image-title').val($image.attr('alt'));
4296
4297 if ($link.length !== 0) {
4298 $('#redactor-image-link').val($link.attr('href'));
4299 if ($link.attr('target') === '_blank') {
4300 $('#redactor-image-link-blank').prop('checked', true);
4301 }
4302 }
4303
4304 // hide link's tooltip
4305 $('.redactor-link-tooltip').remove();
4306
4307 this.modal.show();
4308
4309 // focus
4310 if (this.detect.isDesktop()) {
4311 $('#redactor-image-title').focus();
4312 }
4313
4314 },
4315 update: function () {
4316 var $image = this.observe.image;
4317 var $link = $image.closest('a', this.core.editor()[0]);
4318
4319 var title = $('#redactor-image-title').val().replace(/(<([^>]+)>)/ig, '');
4320 $image.attr('alt', title).attr('title', title);
4321
4322 this.image.setFloating($image);
4323
4324 // as link
4325 var link = $.trim($('#redactor-image-link').val()).replace(/(<([^>]+)>)/ig, '');
4326 if (link !== '') {
4327 // test url (add protocol)
4328 var pattern = '((xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}';
4329 var re = new RegExp('^(http|ftp|https)://' + pattern, 'i');
4330 var re2 = new RegExp('^' + pattern, 'i');
4331
4332 if (link.search(re) === -1 && link.search(re2) === 0 && this.opts.linkProtocol) {
4333 link = this.opts.linkProtocol + '://' + link;
4334 }
4335
4336 var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false;
4337
4338 if ($link.length === 0) {
4339 var a = $('<a href="' + link + '" id="redactor-img-tmp">' + this.utils.getOuterHtml(
4340 $image) + '</a>');
4341 if (target) {
4342 a.attr('target', '_blank');
4343 }
4344
4345 $image = $image.replaceWith(a);
4346 $link = this.core.editor().find('#redactor-img-tmp');
4347 $link.removeAttr('id');
4348 }
4349 else {
4350 $link.attr('href', link);
4351 if (target) {
4352 $link.attr('target', '_blank');
4353 }
4354 else {
4355 $link.removeAttr('target');
4356 }
4357 }
4358 }
4359 else if ($link.length !== 0) {
4360 $link.replaceWith(this.utils.getOuterHtml($image));
4361 }
4362
4363 this.image.addCaption($image, $link);
4364 this.modal.close();
4365
4366 // buffer
4367 this.buffer.set();
4368
4369 },
4370 setFloating: function ($image) {
4371 var $figure = $image.closest('figure', this.$editor[0]);
4372 var $container = ($figure.length !== 0) ? $figure : $image;
4373 var floating = $('#redactor-image-align').val();
4374
4375 var imageFloat = '';
4376 var imageDisplay = '';
4377 var imageMargin = '';
4378 var textAlign = '';
4379
4380 switch (floating) {
4381 case 'left':
4382 imageFloat = 'left';
4383 imageMargin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0';
4384 break;
4385 case 'right':
4386 imageFloat = 'right';
4387 imageMargin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin;
4388 break;
4389 case 'center':
4390
4391 if ($figure.length !== 0) {
4392 textAlign = 'center';
4393 }
4394 else {
4395 imageDisplay = 'block';
4396 imageMargin = 'auto';
4397 }
4398
4399 break;
4400 }
4401
4402 $container.css({
4403 'float': imageFloat,
4404 'display': imageDisplay,
4405 'margin': imageMargin,
4406 'text-align': textAlign
4407 });
4408 $container.attr('rel', $image.attr('style'));
4409 },
4410 addCaption: function ($image, $link) {
4411 var caption = $('#redactor-image-caption').val();
4412
4413 var $target = ($link.length !== 0) ? $link : $image;
4414 var $figcaption = $target.next();
4415
4416 if ($figcaption.length === 0 || $figcaption[0].tagName !== 'FIGCAPTION') {
4417 $figcaption = false;
4418 }
4419
4420 if (caption !== '') {
4421 if ($figcaption === false) {
4422 $figcaption = $('<figcaption />').text(caption);
4423 $target.after($figcaption);
4424 }
4425 else {
4426 $figcaption.text(caption);
4427 }
4428 }
4429 else if ($figcaption !== false) {
4430 $figcaption.remove();
4431 }
4432 },
4433 remove: function (e, $image, index) {
4434 $image = (typeof $image === 'undefined') ? $(this.observe.image) : $image;
4435
4436 // delete from modal
4437 if (typeof e !== 'boolean') {
4438 this.buffer.set();
4439 }
4440
4441 this.events.stopDetectChanges();
4442
4443 var $link = $image.closest('a', this.core.editor()[0]);
4444 var $figure = $image.closest(this.opts.imageTag, this.core.editor()[0]);
4445 var $parent = $image.parent();
4446
4447 // callback
4448 var imageDeleteStop = this.core.callback('imageDelete', e, $image[0]);
4449 if (imageDeleteStop === false) {
4450 if (e) e.preventDefault();
4451 return false;
4452 }
4453
4454 if ($('#redactor-image-box').length !== 0) {
4455 $parent = $('#redactor-image-box').parent();
4456 }
4457
4458 var $next, $prev;
4459 if ($figure.length !== 0) {
4460 $prev = $figure.prev();
4461 $next = $figure.next();
4462 $figure.remove();
4463 }
4464 else if ($link.length !== 0) {
4465 $parent = $link.parent();
4466 $link.remove();
4467 }
4468 else {
4469 $image.remove();
4470 }
4471
4472 $('#redactor-image-box').remove();
4473
4474 if (e !== false) {
4475 if ($next && $next.length !== 0) {
4476 this.caret.start($next);
4477 }
4478 else if ($prev && $prev.length !== 0) {
4479 this.caret.end($prev);
4480 }
4481 }
4482
4483 if (typeof e !== 'boolean') {
4484 this.modal.close();
4485 }
4486
4487 this.utils.restoreScroll();
4488 this.observe.image = false;
4489 this.events.startDetectChanges();
4490 this.code.sync();
4491
4492 }
4493 };
4494 },
4495
4496 // =indent
4497 indent: function () {
4498 return {
4499 increase: function () {
4500 if (!this.list.get()) {
4501 return;
4502 }
4503
4504 var $current = $(this.selection.current()).closest('li');
4505 var $list = $current.closest('ul, ol', this.core.editor()[0]);
4506
4507 var $li = $current.closest('li');
4508 var $prev = $li.prev();
4509 if ($prev.length === 0 || $prev[0].tagName !== 'LI') {
4510 return;
4511 }
4512
4513 this.buffer.set();
4514
4515 if (this.utils.isCollapsed()) {
4516 var listTag = $list[0].tagName;
4517 var $newList = $('<' + listTag + ' />');
4518
4519 this.selection.save();
4520
4521 var $ol = $prev.find('ol').first();
4522 if ($ol.length === 1) {
4523 $ol.append($current);
4524 }
4525 else {
4526 var listTag = $list[0].tagName;
4527 var $newList = $('<' + listTag + ' />');
4528 $newList.append($current);
4529 $prev.append($newList);
4530 }
4531
4532 this.selection.restore();
4533 }
4534 else {
4535 document.execCommand('indent');
4536
4537 // normalize
4538 this.selection.save();
4539 this.indent.removeEmpty();
4540 this.indent.normalize();
4541 this.selection.restore();
4542 }
4543 },
4544 decrease: function () {
4545 if (!this.list.get()) {
4546 return;
4547 }
4548
4549 var $current = $(this.selection.current()).closest('li');
4550 var $list = $current.closest('ul, ol', this.core.editor()[0]);
4551
4552 this.buffer.set();
4553
4554 document.execCommand('outdent');
4555
4556 var $item = $(this.selection.current()).closest('li', this.core.editor()[0]);
4557
4558 if (this.utils.isCollapsed()) {
4559 this.indent.repositionItem($item);
4560 }
4561
4562 var tmpWrapper = null;
4563 if ($item.length === 0) {
4564 // `formatBlock` does not handle custom elements gracefully, always
4565 // treating them as inline elements, causing these to be chopped up
4566 // into separate elements. We can mitigate this problem by introducing
4567 // a temporary intermediate `<div>` which will serve as a block wrapper.
4568 var block = this.selection.block();
4569 if (block && block.nodeName.indexOf('-') !== -1) {
4570 this.selection.save();
4571
4572 tmpWrapper = elCreate('div');
4573 while (block.childNodes.length) {
4574 tmpWrapper.appendChild(block.childNodes[0]);
4575 }
4576 block.appendChild(tmpWrapper);
4577
4578 this.selection.restore();
4579 }
4580
4581 document.execCommand('formatblock', false, 'p');
4582 $item = $(this.selection.current());
4583 var $next = $item.next();
4584 if ($next.length !== 0 && $next[0].tagName === 'BR') {
4585 $next.remove();
4586 }
4587 }
4588
4589 // normalize
4590 this.selection.save();
4591
4592 if (tmpWrapper !== null) {
4593 var parent = tmpWrapper.parentNode;
4594 while (tmpWrapper.childNodes.length) {
4595 parent.insertBefore(tmpWrapper.childNodes[0], tmpWrapper);
4596 }
4597 parent.removeChild(tmpWrapper);
4598 }
4599
4600 this.indent.removeEmpty();
4601 this.indent.normalize();
4602 this.selection.restore();
4603
4604 },
4605 repositionItem: function ($item) {
4606 var $next = $item.next();
4607 if ($next.length !== 0 && ($next[0].tagName !== 'UL' || $next[0].tagName !== 'OL')) {
4608 $item.append($next);
4609 }
4610
4611 var $prev = $item.prev();
4612 if ($prev.length !== 0 && $prev[0].tagName !== 'LI') {
4613 this.selection.save();
4614 var $li = $item.parents('li', this.core.editor()[0]);
4615 $li.after($item);
4616 this.selection.restore();
4617 }
4618 },
4619 normalize: function () {
4620 this.core.editor().find('li').each($.proxy(function (i, s) {
4621 var $el = $(s);
4622
4623 // remove style
4624 var filter = '';
4625 if (this.opts.keepStyleAttr.length !== 0) {
4626 filter = ',' + this.opts.keepStyleAttr.join(',');
4627 }
4628
4629 $el.find(this.opts.inlineTags.join(',')).not('img' + filter).removeAttr(
4630 'style');
4631
4632 var $parent = $el.parent();
4633 if ($parent.length !== 0 && $parent[0].tagName === 'LI') {
4634 $parent.after($el);
4635 return;
4636 }
4637
4638 var $next = $el.next();
4639 if ($next.length !== 0 && ($next[0].tagName === 'UL' || $next[0].tagName === 'OL')) {
4640 $el.append($next);
4641 }
4642
4643 }, this));
4644
4645 },
4646 removeEmpty: function ($list) {
4647 var $lists = this.core.editor().find('ul, ol');
4648 var $items = this.core.editor().find('li');
4649
4650 $items.each($.proxy(function (i, s) {
4651 this.indent.removeItemEmpty(s);
4652
4653 }, this));
4654
4655 $lists.each($.proxy(function (i, s) {
4656 this.indent.removeItemEmpty(s);
4657
4658 }, this));
4659
4660 $items.each($.proxy(function (i, s) {
4661 this.indent.removeItemEmpty(s);
4662
4663 }, this));
4664 },
4665 removeItemEmpty: function (s) {
4666 var html = s.innerHTML.replace(/[\t\s\n]/g, '');
4667 html = html.replace(/<span><\/span>/g, '');
4668
4669 if (html === '') {
4670 $(s).remove();
4671 }
4672 }
4673 };
4674 },
4675
4676 // =inline
4677 inline: function () {
4678 return {
4679 format: function (tag, attr, value, type) {
4680 // Stop formatting pre/code
4681 if (this.utils.isCurrentOrParent(['PRE', 'CODE'])) return;
4682
4683 // Get params
4684 var params = this.inline.getParams(attr, value, type);
4685
4686 // Arrange tag
4687 tag = this.inline.arrangeTag(tag);
4688
4689 this.buffer.set();
4690
4691 (this.utils.isCollapsed()) ? this.inline.formatCollapsed(tag,
4692 params
4693 ) : this.inline.formatUncollapsed(tag, params);
4694 },
4695 formatCollapsed: function (tag, params) {
4696 var newInline;
4697 var inline = this.selection.inline();
4698
4699 if (inline) {
4700 var currentTag = inline.tagName.toLowerCase();
4701 if (currentTag === tag) {
4702 // empty = remove
4703 if (this.utils.isEmpty(inline.innerHTML)) {
4704 this.caret.after(inline);
4705 $(inline).remove();
4706 }
4707 // not empty = break
4708 else {
4709 var $first = this.inline.insertBreakpoint(inline,
4710 currentTag
4711 );
4712 this.caret.after($first);
4713 }
4714 }
4715 else if ($(inline).closest(tag).length === 0) {
4716 newInline = this.inline.insertInline(tag);
4717 newInline = this.inline.setParams(newInline, params);
4718 }
4719 else {
4720 var $first = this.inline.insertBreakpoint(inline, currentTag);
4721 this.caret.after($first);
4722 }
4723 }
4724 else {
4725 newInline = this.inline.insertInline(tag);
4726 newInline = this.inline.setParams(newInline, params);
4727 }
4728 },
4729 formatUncollapsed: function (tag, params) {
4730 var element;
4731
4732 this.selection.save();
4733
4734 var range = window.getSelection().getRangeAt(0);
4735 var contents = range.cloneContents();
4736 if (contents.querySelector(tag) === null) {
4737 element = range.startContainer;
4738 if (element.nodeType === Node.TEXT_NODE) {
4739 element = element.parentElement;
4740 }
4741
4742 var parentWithTheSameTag = element.closest(tag);
4743 if (parentWithTheSameTag !== null && this.core.editor()[0].contains(parentWithTheSameTag)) {
4744 // We need to split the matching parent element
4745 // by moving everything to the left and the right
4746 // into separate nodes.
4747 var leftElement = document.createElement(tag);
4748 parentWithTheSameTag.insertAdjacentElement("beforebegin", leftElement);
4749
4750 var leftRange = document.createRange();
4751 leftRange.selectNodeContents(parentWithTheSameTag);
4752 leftRange.setEnd(range.startContainer, range.startOffset);
4753 leftElement.appendChild(leftRange.extractContents());
4754
4755 var rightElement = document.createElement(tag);
4756 parentWithTheSameTag.insertAdjacentElement("afterend", rightElement);
4757
4758 var rightRange = document.createRange();
4759 rightRange.selectNodeContents(parentWithTheSameTag);
4760 rightRange.setStart(range.endContainer, range.endOffset);
4761 rightElement.appendChild(rightRange.extractContents());
4762
4763 // Finally remove the offending parent element.
4764 var parentElement = parentWithTheSameTag.parentElement;
4765 while (parentWithTheSameTag.childNodes.length) {
4766 parentElement.insertBefore(parentWithTheSameTag.childNodes[0], parentWithTheSameTag);
4767 }
4768 parentWithTheSameTag.remove();
4769
4770 return;
4771 }
4772 }
4773
4774 var nodes = this.inline.getClearedNodes();
4775 this.inline.setNodesStriked(nodes, tag, params);
4776
4777 this.selection.restore();
4778
4779 document.execCommand('strikethrough');
4780
4781 this.selection.saveInstant();
4782
4783 // WoltLab: Chrome misbehaves in some cases, causing the `<strike>` element for
4784 // contained elements to be stripped. Instead, those children are assigned the
4785 // CSS style `text-decoration-line: line-through`.
4786 var chromeElements = this.core.editor()[0].querySelectorAll('[style*="line-through"]'), strike;
4787 for (var i = 0, length = chromeElements.length; i < length; i++) {
4788 element = chromeElements[0];
4789
4790 strike = document.createElement('strike');
4791 element.parentNode.insertBefore(strike, element);
4792 strike.appendChild(element);
4793
4794 // Remove the bogus style attribute.
4795 element.style.removeProperty('text-decoration');
4796 }
4797
4798 var self = this;
4799 this.core.editor().find('strike').each(function () {
4800 var $el = self.utils.replaceToTag(this, tag);
4801 self.inline.setParams($el[0], params);
4802
4803 var $inside = $el.find(tag);
4804 var $parent = $el.parent();
4805 var $parentAround = $parent.parent();
4806
4807 // revert formatting (safari bug)
4808 if ($parentAround.length !== 0 && $parentAround[0].tagName.toLowerCase() === tag && $parentAround.html() == $parent[0].outerHTML) {
4809 $el.replaceWith(function () { return $(this).contents(); });
4810 $parentAround.replaceWith(function () { return $(this).contents(); });
4811
4812 return;
4813 }
4814
4815 // remove inside
4816 if ($inside.length !== 0) {
4817 self.inline.cleanInsideOrParent($inside, params);
4818 }
4819
4820 // same parent
4821 if ($parent.html() == $el[0].outerHTML) {
4822 self.inline.cleanInsideOrParent($parent, params);
4823 }
4824
4825 // bugfix: remove empty inline tags after selection
4826 if (self.detect.isFirefox()) {
4827 self.core.editor().find(tag + ':empty').remove();
4828 }
4829 });
4830
4831 this.selection.restoreInstant();
4832 },
4833 cleanInsideOrParent: function ($el, params) {
4834 if (params) {
4835 for (var key in params.data) {
4836 this.inline.removeSpecificAttr($el, key, params.data[key]);
4837 }
4838 }
4839 },
4840 getClearedNodes: function () {
4841 var nodes = this.selection.nodes();
4842 var newNodes = [];
4843 var len = nodes.length;
4844 var started = 0;
4845
4846 // find array slice
4847 for (var i = 0; i < len; i++) {
4848 if ($(nodes[i]).hasClass('redactor-selection-marker')) {
4849 started = i + 2;
4850 break;
4851 }
4852 }
4853
4854 // find selected inline & text nodes
4855 for (var i = 0; i < len; i++) {
4856 if (i >= started && !this.utils.isBlockTag(nodes[i].tagName)) {
4857 newNodes.push(nodes[i]);
4858 }
4859 }
4860
4861 return newNodes;
4862 },
4863 isConvertableAttr: function (node, name, value) {
4864 var nodeAttrValue = $(node).attr(name);
4865 if (nodeAttrValue) {
4866 if (name === 'style') {
4867 value = $.trim(value).replace(/;$/, '');
4868
4869 var rules = value.split(';');
4870 var count = 0;
4871 for (var i = 0; i < rules.length; i++) {
4872 var arr = rules[i].split(':');
4873 var ruleName = $.trim(arr[0]);
4874 var ruleValue = $.trim(arr[1]);
4875
4876 if (ruleName.search(/color/) !== -1) {
4877 var val = $(node).css(ruleName);
4878 if (val && (val === ruleValue || this.utils.rgb2hex(
4879 val) === ruleValue)) {
4880 count++;
4881 }
4882 }
4883 else if ($(node).css(ruleName) === ruleValue) {
4884 count++;
4885 }
4886 }
4887
4888 if (count === rules.length) {
4889 return 1;
4890 }
4891 }
4892 else if (nodeAttrValue === value) {
4893 return 1;
4894 }
4895 }
4896
4897 return 0;
4898
4899 },
4900 isConvertable: function (node, nodeTag, tag, params) {
4901 if (nodeTag === tag) {
4902 if (params) {
4903 var count = 0;
4904 for (var key in params.data) {
4905 count += this.inline.isConvertableAttr(node,
4906 key,
4907 params.data[key]
4908 );
4909 }
4910
4911 if (count === Object.keys(params.data).length) {
4912 return true;
4913 }
4914 }
4915 else {
4916 return true;
4917 }
4918 }
4919
4920 return false;
4921 },
4922 setNodesStriked: function (nodes, tag, params) {
4923 for (var i = 0; i < nodes.length; i++) {
4924 var nodeTag = (nodes[i].tagName) ? nodes[i].tagName.toLowerCase() : undefined;
4925
4926 var parent = nodes[i].parentNode;
4927 var parentTag = (parent && parent.tagName) ? parent.tagName.toLowerCase() : undefined;
4928
4929 var convertable = this.inline.isConvertable(parent,
4930 parentTag,
4931 tag,
4932 params
4933 );
4934 if (convertable) {
4935 var $el = $(parent).replaceWith(function () {
4936 return $('<strike>').append($(this).contents());
4937 });
4938
4939 $el.attr('data-redactor-inline-converted');
4940 }
4941
4942 var convertable = this.inline.isConvertable(nodes[i],
4943 nodeTag,
4944 tag,
4945 params
4946 );
4947 if (convertable) {
4948 var $el = $(nodes[i]).replaceWith(function () {
4949 return $('<strike>').append($(this).contents());
4950 });
4951 }
4952 }
4953 },
4954 insertBreakpoint: function (inline, currentTag) {
4955 var breakpoint = document.createElement('span');
4956 breakpoint.id = 'redactor-inline-breakpoint';
4957 breakpoint = this.insert.node(breakpoint);
4958
4959 var end = this.utils.isEndOfElement(inline);
4960 var code = this.utils.getOuterHtml(inline);
4961 var endTag = (end) ? '' : '<' + currentTag + '>';
4962
4963 code = code.replace(/<span id="redactor-inline-breakpoint"><\/span>/i,
4964 '</' + currentTag + '>' + endTag
4965 );
4966
4967 var $code = $(code);
4968 $(inline).replaceWith($code);
4969
4970 if (endTag !== '') {
4971 this.utils.cloneAttributes(inline, $code.last());
4972 }
4973
4974 return $code.first();
4975 },
4976 insertInline: function (tag) {
4977 var node = document.createElement(tag);
4978
4979 this.insert.node(node);
4980 this.caret.start(node);
4981
4982 return node;
4983 },
4984 arrangeTag: function (tag) {
4985 var tags = [
4986 'b',
4987 'bold',
4988 'i',
4989 'italic',
4990 'underline',
4991 'strikethrough',
4992 'deleted',
4993 'superscript',
4994 'subscript'
4995 ];
4996 var replaced = [
4997 'strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub'
4998 ];
4999
5000 tag = tag.toLowerCase();
5001
5002 for (var i = 0; i < tags.length; i++) {
5003 if (tag === tags[i]) {
5004 tag = replaced[i];
5005 }
5006 }
5007
5008 return tag;
5009 },
5010 getStyleParams: function (params) {
5011 var result = {};
5012 var rules = params.trim().replace(/;$/, '').split(';');
5013 for (var i = 0; i < rules.length; i++) {
5014 var rule = rules[i].split(':');
5015 if (rule) {
5016 result[rule[0].trim()] = rule[1].trim();
5017 }
5018 }
5019
5020 return result;
5021 },
5022 getParams: function (attr, value, type) {
5023 var data = false;
5024 var func = 'toggle';
5025 if (typeof attr === 'object') {
5026 data = attr;
5027 func = (value !== undefined) ? value : func;
5028 }
5029 else if (attr !== undefined && value !== undefined) {
5030 data = {};
5031 data[attr] = value;
5032 func = (type !== undefined) ? type : func;
5033 }
5034
5035 return (data) ? {
5036 'func': func,
5037 'data': data
5038 } : false;
5039 },
5040 setParams: function (node, params) {
5041 if (params) {
5042 for (var key in params.data) {
5043 var $node = $(node);
5044 if (key === 'style') {
5045 node = this.inline[params.func + 'Style'](params.data[key],
5046 node
5047 );
5048 $node.attr('data-redactor-style-cache',
5049 $node.attr('style')
5050 );
5051 }
5052 else if (key === 'class') {
5053 node = this.inline[params.func + 'Class'](params.data[key],
5054 node
5055 );
5056 }
5057 // attr
5058 else {
5059 node = (params.func === 'remove') ? this.inline[params.func + 'Attr'](key,
5060 node
5061 ) : this.inline[params.func + 'Attr'](key,
5062 params.data[key],
5063 node
5064 );
5065 }
5066
5067 if (key === 'style' && node.tagName === 'SPAN') {
5068 $node.attr('data-redactor-span', true);
5069 }
5070 }
5071 }
5072
5073 return node;
5074 },
5075
5076 // Each
5077 eachInline: function (node, callback) {
5078 var lastNode;
5079 var nodes = (node === undefined) ? this.selection.inlines() : [node];
5080 if (nodes) {
5081 for (var i = 0; i < nodes.length; i++) {
5082 lastNode = callback(nodes[i])[0];
5083 }
5084 }
5085
5086 return lastNode;
5087 },
5088
5089 // Class
5090 replaceClass: function (value, node) {
5091 return this.inline.eachInline(node, function (el) {
5092 return $(el).removeAttr('class').addClass(value);
5093 });
5094 },
5095 toggleClass: function (value, node) {
5096 return this.inline.eachInline(node, function (el) {
5097 return $(el).toggleClass(value);
5098 });
5099 },
5100 addClass: function (value, node) {
5101 return this.inline.eachInline(node, function (el) {
5102 return $(el).addClass(value);
5103 });
5104 },
5105 removeClass: function (value, node) {
5106 return this.inline.eachInline(node, function (el) {
5107 return $(el).removeClass(value);
5108 });
5109 },
5110 removeAllClass: function (node) {
5111 return this.inline.eachInline(node, function (el) {
5112 return $(el).removeAttr('class');
5113 });
5114 },
5115
5116 // Attr
5117 replaceAttr: function (name, value, node) {
5118 return this.inline.eachInline(node, function (el) {
5119 return $(el).removeAttr(name).attr(name.value);
5120 });
5121 },
5122 toggleAttr: function (name, value, node) {
5123 return this.inline.eachInline(node, function (el) {
5124 var attr = $(el).attr(name);
5125
5126 return (attr) ? $(el).removeAttr(name) : $(el).attr(name.value);
5127 });
5128 },
5129 addAttr: function (name, value, node) {
5130 return this.inline.eachInline(node, function (el) {
5131 return $(el).attr(name, value);
5132 });
5133 },
5134 removeAttr: function (name, node) {
5135 return this.inline.eachInline(node, function (el) {
5136 var $el = $(el);
5137
5138 $el.removeAttr(name);
5139 if (name === 'style') {
5140 $el.removeAttr('data-redactor-style-cache');
5141 }
5142
5143 return $el;
5144 });
5145 },
5146 removeAllAttr: function (node) {
5147 return this.inline.eachInline(node, function (el) {
5148 var $el = $(el);
5149 var len = el.attributes.length;
5150 for (var z = 0; z < len; z++) {
5151 $el.removeAttr(el.attributes[z].name);
5152 }
5153
5154 return $el;
5155 });
5156 },
5157 removeSpecificAttr: function (node, key, value) {
5158 var $el = $(node);
5159 if (key === 'style') {
5160 var arr = value.split(':');
5161 var name = arr[0].trim();
5162 $el.css(name, '');
5163
5164 if (this.utils.removeEmptyAttr(node, 'style')) {
5165 $el.removeAttr('data-redactor-style-cache');
5166 }
5167 }
5168 else {
5169 $el.removeAttr(key)[0];
5170 }
5171 },
5172
5173 // Style
5174 hasParentStyle: function ($el) {
5175 var $parent = $el.parent();
5176
5177 return ($parent.length === 1 && $parent[0].tagName === $el[0].tagName && $parent.html() === $el[0].outerHTML) ? $parent : false;
5178 },
5179 addParentStyle: function ($el) {
5180 var $parent = this.inline.hasParentStyle($el);
5181 if ($parent) {
5182 var style = this.inline.getStyleParams($el.attr('style'));
5183 $parent.css(style);
5184 $parent.attr('data-redactor-style-cache', $parent.attr('style'));
5185
5186 $el.replaceWith(function () {
5187 return $(this).contents();
5188 });
5189 }
5190 else {
5191 $el.attr('data-redactor-style-cache', $el.attr('style'));
5192 }
5193
5194 return $el;
5195 },
5196 replaceStyle: function (params, node) {
5197 params = this.inline.getStyleParams(params);
5198
5199 var self = this;
5200 return this.inline.eachInline(node, function (el) {
5201 var $el = $(el);
5202 $el.removeAttr('style').css(params);
5203
5204 var style = $el.attr('style');
5205 if (style) $el.attr('style', style.replace(/"/g, '\''));
5206
5207 $el = self.inline.addParentStyle($el);
5208
5209 return $el;
5210 });
5211 },
5212 toggleStyle: function (params, node) {
5213 params = this.inline.getStyleParams(params);
5214
5215 var self = this;
5216 return this.inline.eachInline(node, function (el) {
5217 var $el = $(el);
5218
5219 for (var key in params) {
5220 var newVal = params[key];
5221 var oldVal = $el.css(key);
5222
5223 oldVal = (self.utils.isRgb(oldVal)) ? self.utils.rgb2hex(oldVal) : oldVal.replace(/"/g,
5224 ''
5225 );
5226 newVal = (self.utils.isRgb(newVal)) ? self.utils.rgb2hex(newVal) : newVal.replace(/"/g,
5227 ''
5228 );
5229
5230 if (oldVal === newVal) {
5231 $el.css(key, '');
5232 }
5233 else {
5234 $el.css(key, newVal);
5235 }
5236 }
5237
5238 var style = $el.attr('style');
5239 if (style) $el.attr('style', style.replace(/"/g, '\''));
5240
5241 if (!self.utils.removeEmptyAttr(el, 'style')) {
5242 $el = self.inline.addParentStyle($el);
5243 }
5244 else {
5245 $el.removeAttr('data-redactor-style-cache');
5246 }
5247
5248 return $el;
5249 });
5250 },
5251 addStyle: function (params, node) {
5252 params = this.inline.getStyleParams(params);
5253
5254 var self = this;
5255 return this.inline.eachInline(node, function (el) {
5256 var $el = $(el);
5257
5258 $el.css(params);
5259
5260 var style = $el.attr('style');
5261 if (style) $el.attr('style', style.replace(/"/g, '\''));
5262
5263 $el = self.inline.addParentStyle($el);
5264
5265 return $el;
5266 });
5267 },
5268 removeStyle: function (params, node) {
5269 params = this.inline.getStyleParams(params);
5270
5271 var self = this;
5272 return this.inline.eachInline(node, function (el) {
5273 var $el = $(el);
5274
5275 for (var key in params) {
5276 $el.css(key, '');
5277 }
5278
5279 if (self.utils.removeEmptyAttr(el, 'style')) {
5280 $el.removeAttr('data-redactor-style-cache');
5281 }
5282 else {
5283 $el.attr('data-redactor-style-cache', $el.attr('style'));
5284 }
5285
5286 return $el;
5287 });
5288 },
5289 removeAllStyle: function (node) {
5290 return this.inline.eachInline(node, function (el) {
5291 return $(el).removeAttr('style').removeAttr('data-redactor-style-cache');
5292 });
5293 },
5294 removeStyleRule: function (name) {
5295 var parent = this.selection.parent();
5296 var nodes = this.selection.inlines();
5297
5298 this.buffer.set();
5299
5300 if (parent && parent.tagName === 'SPAN') {
5301 this.inline.removeStyleRuleAttr($(parent), name);
5302 }
5303
5304 for (var i = 0; i < nodes.length; i++) {
5305 var el = nodes[i];
5306 var $el = $(el);
5307 if ($.inArray(el.tagName.toLowerCase(),
5308 this.opts.inlineTags
5309 ) != -1 && !$el.hasClass('redactor-selection-marker')) {
5310 this.inline.removeStyleRuleAttr($el, name);
5311 }
5312 }
5313
5314 },
5315 removeStyleRuleAttr: function ($el, name) {
5316 $el.css(name, '');
5317 if (this.utils.removeEmptyAttr($el, 'style')) {
5318 $el.removeAttr('data-redactor-style-cache');
5319 }
5320 else {
5321 $el.attr('data-redactor-style-cache', $el.attr('style'));
5322 }
5323 },
5324
5325 // Update
5326 update: function (tag, attr, value, type) {
5327 tag = this.inline.arrangeTag(tag);
5328
5329 var params = this.inline.getParams(attr, value, type);
5330 var nodes = this.selection.inlines();
5331 var result = [];
5332
5333 if (nodes) {
5334 for (var i = 0; i < nodes.length; i++) {
5335 var el = nodes[i];
5336 if (tag === '*' || el.tagName.toLowerCase() === tag) {
5337 result.push(this.inline.setParams(el, params));
5338 }
5339 }
5340 }
5341
5342 return result;
5343 },
5344
5345 // All
5346 removeFormat: function () {
5347 this.selection.save();
5348
5349 var nodes = this.inline.getClearedNodes();
5350 for (var i = 0; i < nodes.length; i++) {
5351 if (nodes[i].nodeType === 1) {
5352 $(nodes[i]).replaceWith(function () {
5353 return $(this).contents();
5354 });
5355 }
5356 }
5357
5358 this.selection.restore();
5359 }
5360
5361 };
5362 },
5363
5364 // =insert
5365 insert: function () {
5366 return {
5367 set: function (html) {
5368 this.code.set(html);
5369 this.focus.end();
5370 },
5371 html: function (html, data) {
5372 this.core.editor().focus();
5373
5374 var block = this.selection.block();
5375 var inline = this.selection.inline();
5376
5377 // clean
5378 if (typeof data === 'undefined') {
5379 data = this.clean.getCurrentType(html, true);
5380 html = this.clean.onPaste(html, data, true);
5381 }
5382
5383 html = $.parseHTML(html);
5384
5385 // end node
5386 var endNode = $(html).last();
5387
5388 // delete selected content
5389 var sel = this.selection.get();
5390 var range = this.selection.range(sel);
5391 range.deleteContents();
5392
5393 this.selection.update(sel, range);
5394
5395 // insert list in list
5396 if (data.lists) {
5397 var $list = $(html);
5398 if ($list.length !== 0 && ($list[0].tagName === 'UL' || $list[0].tagName === 'OL')) {
5399
5400 this.insert.appendLists(block, $list);
5401 return;
5402 }
5403 }
5404
5405 if (data.blocks && block) {
5406 if (this.utils.isSelectAll()) {
5407 this.core.editor().html(html);
5408 this.focus.end();
5409 }
5410 else {
5411 var breaked = this.utils.breakBlockTag();
5412 if (breaked === false) {
5413 this.insert.placeHtml(html);
5414 }
5415 else {
5416 var $last = $(html).children().last();
5417 $last.append(this.marker.get());
5418
5419 if (breaked.type === 'start') {
5420 breaked.$block.before(html);
5421 }
5422 else {
5423 breaked.$block.after(html);
5424 }
5425
5426 this.selection.restore();
5427 this.core.editor().find('p').each(function () {
5428 if ($.trim(this.innerHTML) === '') {
5429 $(this).remove();
5430 }
5431 });
5432 }
5433 }
5434 }
5435 else {
5436 if (inline) {
5437 // remove same tag inside
5438 var $div = $('<div/>').html(html);
5439 $div.find(inline.tagName.toLowerCase()).each(function () {
5440 $(this).contents().unwrap();
5441 });
5442
5443 html = $div.html();
5444 html = $.parseHTML(html);
5445
5446 endNode = $(html).last();
5447
5448 }
5449
5450 if (this.utils.isSelectAll()) {
5451 var $node = $(this.opts.emptyHtml);
5452 this.core.editor().html('').append($node);
5453 $node.html(html);
5454 this.caret.end($node);
5455 }
5456 else {
5457 this.insert.placeHtml(html);
5458 }
5459 }
5460
5461 this.utils.disableSelectAll();
5462
5463 if (data.pre) this.clean.cleanPre();
5464
5465 this.caret.end(endNode);
5466 },
5467 text: function (text) {
5468 text = text.toString();
5469 text = $.trim(text);
5470
5471 var tmp = document.createElement('div');
5472 tmp.innerHTML = text;
5473 text = tmp.textContent || tmp.innerText;
5474
5475 if (typeof text === 'undefined') {
5476 return;
5477 }
5478
5479 this.core.editor().focus();
5480
5481 // blocks
5482 var blocks = this.selection.blocks();
5483
5484 // nl to spaces
5485 text = text.replace(/\n/g, ' ');
5486
5487 // select all
5488 if (this.utils.isSelectAll()) {
5489 var $node = $(this.opts.emptyHtml);
5490 this.core.editor().html('').append($node);
5491 $node.html(text);
5492 this.caret.end($node);
5493 }
5494 else {
5495 // insert
5496 var sel = this.selection.get();
5497 var node = document.createTextNode(text);
5498
5499 if (sel.getRangeAt && sel.rangeCount) {
5500 var range = sel.getRangeAt(0);
5501 range.deleteContents();
5502 range.insertNode(node);
5503 range.setStartAfter(node);
5504 range.collapse(true);
5505
5506 this.selection.update(sel, range);
5507 }
5508
5509 // wrap node if selected two or more block tags
5510 if (blocks.length > 1) {
5511 $(node).wrap('<p>');
5512 this.caret.after(node);
5513 }
5514 }
5515
5516 this.utils.disableSelectAll();
5517 this.clean.normalizeCurrentHeading();
5518
5519 },
5520 raw: function (html) {
5521 this.core.editor().focus();
5522
5523 var sel = this.selection.get();
5524
5525 var range = this.selection.range(sel);
5526 range.deleteContents();
5527
5528 var el = document.createElement('div');
5529 el.innerHTML = html;
5530
5531 var frag = document.createDocumentFragment(), node, lastNode;
5532 while ((node = el.firstChild)) {
5533 lastNode = frag.appendChild(node);
5534 }
5535
5536 range.insertNode(frag);
5537
5538 if (lastNode) {
5539 range = range.cloneRange();
5540 range.setStartAfter(lastNode);
5541 range.collapse(true);
5542 sel.removeAllRanges();
5543 sel.addRange(range);
5544 }
5545 },
5546 node: function (node, deleteContent) {
5547 if (typeof this.start !== 'undefined') {
5548 this.core.editor().focus();
5549 }
5550
5551 node = node[0] || node;
5552
5553 var block = this.selection.block();
5554 var gap = this.utils.isBlockTag(node.tagName);
5555 var result = true;
5556
5557 if (this.utils.isSelectAll()) {
5558 if (gap) {
5559 this.core.editor().html(node);
5560 }
5561 else {
5562 this.core.editor().html($('<p>').html(node));
5563 }
5564
5565 this.code.sync();
5566 }
5567 else if (gap && block) {
5568 var breaked = this.utils.breakBlockTag();
5569 if (breaked === false) {
5570 this.insert.placeNode(node, deleteContent);
5571 }
5572 else {
5573 if (breaked.type === 'start') {
5574 breaked.$block.before(node);
5575 }
5576 else {
5577 breaked.$block.after(node);
5578 }
5579
5580 this.core.editor().find('p:empty').remove();
5581 }
5582 }
5583 else {
5584 result = this.insert.placeNode(node, deleteContent);
5585 }
5586
5587 this.utils.disableSelectAll();
5588
5589 if (result) {
5590 this.caret.end(node);
5591 }
5592
5593 return node;
5594
5595 },
5596 appendLists: function (block, $list) {
5597 var $block = $(block);
5598 var last;
5599 var isEmpty = this.utils.isEmpty(block.innerHTML);
5600
5601 if (isEmpty || this.utils.isEndOfElement(block)) {
5602 last = $block;
5603 $list.find('li').each(function () {
5604 last.after(this);
5605 last = $(this);
5606 });
5607
5608 if (isEmpty) {
5609 $block.remove();
5610 }
5611 }
5612 else if (this.utils.isStartOfElement(block)) {
5613 $list.find('li').each(function () {
5614 $block.before(this);
5615 last = $(this);
5616 });
5617 }
5618 else {
5619 var endOfNode = this.selection.extractEndOfNode(block);
5620
5621 $block.after($('<li>').append(endOfNode));
5622 $block.append($list);
5623 last = $list;
5624 }
5625
5626 this.marker.remove();
5627
5628 if (last) {
5629 this.caret.end(last);
5630 }
5631 },
5632 placeHtml: function (html) {
5633 var marker = document.createElement('span');
5634 marker.id = 'redactor-insert-marker';
5635 marker = this.insert.node(marker);
5636
5637 $(marker).before(html);
5638 this.selection.restore();
5639 this.caret.after(marker);
5640 $(marker).remove();
5641 },
5642 placeNode: function (node, deleteContent) {
5643 var sel = this.selection.get();
5644 var range = this.selection.range(sel);
5645 if (range == null) {
5646 return false;
5647 }
5648
5649 if (deleteContent !== false) {
5650 range.deleteContents();
5651 }
5652
5653 range.insertNode(node);
5654 range.collapse(false);
5655
5656 this.selection.update(sel, range);
5657 },
5658 nodeToPoint: function (e, node) {
5659 node = node[0] || node;
5660
5661 if (this.utils.isEmpty()) {
5662 node = (this.utils.isBlock(node)) ? node : $('<p />').append(node);
5663
5664 this.core.editor().html(node);
5665
5666 return node;
5667 }
5668
5669 var range;
5670 var x = e.clientX, y = e.clientY;
5671 if (document.caretPositionFromPoint) {
5672 var pos = document.caretPositionFromPoint(x, y);
5673 var sel = document.getSelection();
5674 range = sel.getRangeAt(0);
5675 range.setStart(pos.offsetNode, pos.offset);
5676 range.collapse(true);
5677 range.insertNode(node);
5678 }
5679 else if (document.caretRangeFromPoint) {
5680 range = document.caretRangeFromPoint(x, y);
5681 range.insertNode(node);
5682 }
5683 else if (typeof document.body.createTextRange !== 'undefined') {
5684 range = document.body.createTextRange();
5685 range.moveToPoint(x, y);
5686 var endRange = range.duplicate();
5687 endRange.moveToPoint(x, y);
5688 range.setEndPoint('EndToEnd', endRange);
5689 range.select();
5690 }
5691
5692 return node;
5693
5694 },
5695
5696 // #backward
5697 nodeToCaretPositionFromPoint: function (e, node) {
5698 this.insert.nodeToPoint(e, node);
5699 },
5700 marker: function () {
5701 this.marker.insert();
5702 }
5703 };
5704 },
5705
5706 // =keydown
5707 keydown: function () {
5708 return {
5709 init: function (e) {
5710 if (this.rtePaste) {
5711 return;
5712 }
5713
5714 var key = e.which;
5715 var arrow = (key >= 37 && key <= 40);
5716
5717 this.keydown.ctrl = e.ctrlKey || e.metaKey;
5718 this.keydown.parent = this.selection.parent();
5719 this.keydown.current = this.selection.current();
5720 this.keydown.block = this.selection.block();
5721
5722 // detect tags
5723 this.keydown.pre = this.utils.isTag(this.keydown.current, 'pre');
5724 this.keydown.blockquote = this.utils.isTag(this.keydown.current, 'blockquote');
5725 this.keydown.figcaption = this.utils.isTag(this.keydown.current, 'figcaption');
5726 this.keydown.figure = this.utils.isTag(this.keydown.current, 'figure');
5727
5728 // callback
5729 var keydownStop = this.core.callback('keydown', e);
5730 if (keydownStop === false) {
5731 e.preventDefault();
5732 return false;
5733 }
5734
5735 // shortcuts setup
5736 this.shortcuts.init(e, key);
5737
5738 // buffer
5739 this.keydown.checkEvents(arrow, key);
5740 this.keydown.setupBuffer(e, key);
5741
5742 if (this.utils.isSelectAll() && (key === this.keyCode.ENTER || key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE)) {
5743 e.preventDefault();
5744
5745 this.code.set(this.opts.emptyHtml);
5746 this.events.changeHandler();
5747 return;
5748 }
5749
5750 this.keydown.addArrowsEvent(arrow);
5751 this.keydown.setupSelectAll(e, key);
5752
5753 // turn off enter key
5754 if (!this.opts.enterKey && key === this.keyCode.ENTER) {
5755 e.preventDefault();
5756
5757 // remove selected
5758 var sel = this.selection.get();
5759 var range = this.selection.range(sel);
5760
5761 if (!range.collapsed) {
5762 range.deleteContents();
5763 }
5764
5765 return;
5766 }
5767
5768 // down
5769 if (this.opts.enterKey && key === this.keyCode.DOWN) {
5770 this.keydown.onArrowDown();
5771 }
5772
5773 // up
5774 if (this.opts.enterKey && key === this.keyCode.UP) {
5775 this.keydown.onArrowUp();
5776 }
5777
5778 // replace to p before / after the table or into body
5779 if ((this.opts.type === 'textarea' || this.opts.type === 'div') && this.keydown.current && this.keydown.current.nodeType === 3 && $(
5780 this.keydown.parent).hasClass('redactor-in')) {
5781 this.keydown.wrapToParagraph();
5782 }
5783
5784 // on Shift+Space or Ctrl+Space
5785 if (!this.keyup.lastShiftKey && key === this.keyCode.SPACE && (e.ctrlKey || e.shiftKey)) {
5786 e.preventDefault();
5787
5788 return this.keydown.onShiftSpace();
5789 }
5790
5791 // on Shift+Enter or Ctrl+Enter
5792 if (key === this.keyCode.ENTER && (e.ctrlKey || e.shiftKey)) {
5793 // iOS Safari will report the shift key to be pressed, if the caret is at the
5794 // front of the line and the next character should be an uppercase character.
5795 if (Environment === null || Environment.platform() !== 'ios') {
5796 e.preventDefault();
5797
5798 return this.keydown.onShiftEnter(e);
5799 }
5800 }
5801
5802 // on enter
5803 if (key === this.keyCode.ENTER && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
5804 return this.keydown.onEnter(e);
5805 }
5806
5807 // tab or cmd + [
5808 if (key === this.keyCode.TAB || e.metaKey && key === 221 || e.metaKey && key === 219) {
5809 return this.keydown.onTab(e, key);
5810 }
5811
5812 // firefox bugfix
5813 if (this.detect.isFirefox() && key === this.keyCode.BACKSPACE && this.keydown.block && this.keydown.block.tagName === 'P' && this.utils.isStartOfElement(
5814 this.keydown.block)) {
5815 var $prev = $(this.keydown.block).prev();
5816 if ($prev.length !== 0) {
5817 e.preventDefault();
5818
5819 $prev.append(this.marker.get());
5820 $prev.append($(this.keydown.block).html());
5821 $(this.keydown.block).remove();
5822
5823 this.selection.restore();
5824
5825 return;
5826 }
5827 }
5828
5829 // backspace & delete
5830 if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) {
5831 if (this.observe.image && typeof this.observe.image !== 'undefined' && $(
5832 '#redactor-image-box').length !== 0) {
5833 e.preventDefault();
5834
5835 var $prev = this.observe.image.closest('figure, p').prev();
5836 this.image.remove(false);
5837 this.observe.image = false;
5838
5839 if ($prev && $prev.length !== 0) {
5840 this.caret.end($prev);
5841 }
5842 else {
5843 this.core.editor().focus();
5844 }
5845
5846 return;
5847 }
5848
5849 this.keydown.onBackspaceAndDeleteBefore();
5850 }
5851
5852 if (key === this.keyCode.DELETE) {
5853 var $next = $(this.keydown.block).next();
5854
5855 // delete figure
5856 if (this.utils.isEndOfElement(this.keydown.block) && $next.length !== 0 && $next[0].tagName === 'FIGURE') {
5857 $next.remove();
5858 return false;
5859 }
5860
5861 // append list (safari bug)
5862 var tagLi = (this.keydown.block && this.keydown.block.tagName === 'LI') ? this.keydown.block : false;
5863 if (tagLi) {
5864 var $list = $(this.keydown.block).parents('ul, ol').last();
5865 var $nextList = $list.next();
5866
5867 if (this.utils.isRedactorParent($list) && this.utils.isEndOfElement(
5868 $list) && $nextList.length !== 0 && ($nextList[0].tagName === 'UL' || $nextList[0].tagName === 'OL')) {
5869 e.preventDefault();
5870
5871 $list.append($nextList.contents());
5872 $nextList.remove();
5873
5874 return false;
5875 }
5876 }
5877
5878 // append pre
5879 if (this.utils.isEndOfElement(this.keydown.block) && $next.length !== 0 && $next[0].tagName === 'PRE') {
5880 $(this.keydown.block).append($next.text());
5881 $next.remove();
5882 return false;
5883 }
5884
5885 }
5886
5887 // image delete
5888 if (key === this.keyCode.DELETE && $('#redactor-image-box').length !== 0) {
5889 this.image.remove();
5890 }
5891
5892 // backspace
5893 if (key === this.keyCode.BACKSPACE) {
5894 if (this.detect.isFirefox()) {
5895 this.line.removeOnBackspace(e);
5896 }
5897
5898 // combine list after and before if paragraph is empty
5899 if (this.list.combineAfterAndBefore(this.keydown.block)) {
5900 e.preventDefault();
5901 return;
5902 }
5903
5904 // backspace as outdent
5905 var block = this.selection.block();
5906 if (block && block.tagName === 'LI' && this.utils.isCollapsed() && this.utils.isStartOfElement()) {
5907 this.indent.decrease();
5908 e.preventDefault();
5909 return;
5910 }
5911
5912 this.keydown.removeInvisibleSpace();
5913 this.keydown.removeEmptyListInTable(e);
5914
5915 }
5916
5917 if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) {
5918 this.keydown.onBackspaceAndDeleteAfter(e);
5919 }
5920
5921 },
5922 onShiftSpace: function () {
5923 this.buffer.set();
5924 this.insert.raw('&nbsp;');
5925
5926 return false;
5927 },
5928 onShiftEnter: function (e) {
5929 this.buffer.set();
5930
5931 return (this.keydown.pre) ? this.keydown.insertNewLine(e) : this.insert.raw(
5932 '<br>');
5933 },
5934 onBackspaceAndDeleteBefore: function () {
5935 this.utils.saveScroll();
5936 },
5937 onBackspaceAndDeleteAfter: function (e) {
5938 // remove style tag
5939 setTimeout($.proxy(function () {
5940 this.code.syncFire = false;
5941 this.keydown.removeEmptyLists();
5942
5943 var filter = '';
5944 if (this.opts.keepStyleAttr.length !== 0) {
5945 filter = ',' + this.opts.keepStyleAttr.join(',');
5946 }
5947
5948 var $styleTags = this.core.editor().find('*[style]');
5949 $styleTags.not(
5950 'img, figure, iframe, #redactor-image-box, #redactor-image-editter, [data-redactor-style-cache], [data-redactor-span]' + filter).removeAttr(
5951 'style');
5952
5953 this.keydown.formatEmpty(e);
5954 this.code.syncFire = true;
5955
5956 }, this), 1);
5957 },
5958 onEnter: function (e) {
5959 var stop = this.core.callback('enter', e);
5960 if (stop === false) {
5961 e.preventDefault();
5962 return false;
5963 }
5964
5965 // blockquote exit
5966 if (this.keydown.blockquote && this.keydown.exitFromBlockquote(e) === true) {
5967 return false;
5968 }
5969
5970 // pre
5971 if (this.keydown.pre) {
5972 return this.keydown.insertNewLine(e);
5973 }
5974 // blockquote & figcaption
5975 else if (this.keydown.blockquote || this.keydown.figcaption) {
5976 return this.keydown.insertBreakLine(e);
5977 }
5978 // figure
5979 else if (this.keydown.figure) {
5980 setTimeout($.proxy(function () {
5981 this.keydown.replaceToParagraph('FIGURE');
5982
5983 }, this), 1);
5984 }
5985 // paragraphs
5986 else if (this.keydown.block) {
5987 setTimeout($.proxy(function () {
5988 this.keydown.replaceToParagraph('DIV');
5989
5990 }, this), 1);
5991
5992 // empty list exit
5993 if (this.keydown.block.tagName === 'LI') {
5994 var current = this.selection.current();
5995 var $parent = $(current).closest('li', this.$editor[0]);
5996 var $list = $parent.parents('ul,ol', this.$editor[0]).last();
5997
5998 if ($parent.length !== 0 && this.utils.isEmpty($parent.html()) && $list.next().length === 0 && this.utils.isEmpty(
5999 $list.find('li').last().html())) {
6000 $list.find('li').last().remove();
6001
6002 var node = $(this.opts.emptyHtml);
6003 $list.after(node);
6004 this.caret.start(node);
6005
6006 return false;
6007 }
6008 }
6009
6010 }
6011 // outside
6012 else if (!this.keydown.block) {
6013 return this.keydown.insertParagraph(e);
6014 }
6015
6016 // firefox enter into inline element
6017 if (this.detect.isFirefox() && this.utils.isInline(this.keydown.parent)) {
6018 this.keydown.insertBreakLine(e);
6019 return;
6020 }
6021
6022 // remove inline tags in new-empty paragraph
6023 if (!this.opts.keepInlineOnEnter) {
6024 setTimeout($.proxy(function () {
6025 var inline = this.selection.inline();
6026 if (inline && this.utils.isEmpty(inline.innerHTML)) {
6027 var parent = this.selection.block();
6028 $(inline).remove();
6029 //this.caret.start(parent);
6030
6031 var range = document.createRange();
6032 range.setStart(parent, 0);
6033
6034 var textNode = document.createTextNode('\u200B');
6035
6036 range.insertNode(textNode);
6037 range.setStartAfter(textNode);
6038 range.collapse(true);
6039
6040 var sel = window.getSelection();
6041 sel.removeAllRanges();
6042 sel.addRange(range);
6043 }
6044
6045 }, this), 1);
6046 }
6047 },
6048 checkEvents: function (arrow, key) {
6049 if (!arrow && (this.core.getEvent() === 'click' || this.core.getEvent() === 'arrow')) {
6050 this.core.addEvent(false);
6051
6052 if (this.keydown.checkKeyEvents(key)) {
6053 this.buffer.set();
6054 }
6055 }
6056 },
6057 checkKeyEvents: function (key) {
6058 var k = this.keyCode;
6059 var keys = [
6060 k.BACKSPACE,
6061 k.DELETE,
6062 k.ENTER,
6063 k.ESC,
6064 k.TAB,
6065 k.CTRL,
6066 k.META,
6067 k.ALT,
6068 k.SHIFT
6069 ];
6070
6071 return ($.inArray(key, keys) === -1) ? true : false;
6072
6073 },
6074 addArrowsEvent: function (arrow) {
6075 if (!arrow) {
6076 return;
6077 }
6078
6079 if ((this.core.getEvent() === 'click' || this.core.getEvent() === 'arrow')) {
6080 this.core.addEvent(false);
6081 return;
6082 }
6083
6084 this.core.addEvent('arrow');
6085 },
6086 setupBuffer: function (e, key) {
6087 if (this.keydown.ctrl && key === 90 && !e.shiftKey && !e.altKey && this.sBuffer.length) // z key
6088 {
6089 e.preventDefault();
6090 this.buffer.undo();
6091 return;
6092 }
6093 // redo
6094 else if (this.keydown.ctrl && key === 90 && e.shiftKey && !e.altKey && this.sRebuffer.length !== 0) {
6095 e.preventDefault();
6096 this.buffer.redo();
6097 return;
6098 }
6099 else if (!this.keydown.ctrl) {
6100 if (key === this.keyCode.SPACE || key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE || (key === this.keyCode.ENTER && !e.ctrlKey && !e.shiftKey)) {
6101 this.buffer.set();
6102 }
6103 }
6104 },
6105 exitFromBlockquote: function (e) {
6106 if (!this.utils.isEndOfElement(this.keydown.blockquote)) {
6107 return;
6108 }
6109
6110 var tmp = this.clean.removeSpacesHard($(this.keydown.blockquote).html());
6111 if (tmp.search(/(<br\s?\/?>){1}$/i) !== -1) {
6112 e.preventDefault();
6113
6114 var $last = $(this.keydown.blockquote).children().last();
6115
6116 $last.filter('br').remove();
6117 $(this.keydown.blockquote).children().last().filter('span').remove();
6118
6119 var node = $(this.opts.emptyHtml);
6120 $(this.keydown.blockquote).after(node);
6121 this.caret.start(node);
6122
6123 return true;
6124
6125 }
6126
6127 return;
6128 },
6129 onArrowDown: function () {
6130 var tags = [this.keydown.blockquote, this.keydown.pre, this.keydown.figcaption];
6131
6132 for (var i = 0; i < tags.length; i++) {
6133 if (tags[i]) {
6134 this.keydown.insertAfterLastElement(tags[i]);
6135 return false;
6136 }
6137 }
6138 },
6139 onArrowUp: function () {
6140 var tags = [this.keydown.blockquote, this.keydown.pre, this.keydown.figcaption];
6141
6142 for (var i = 0; i < tags.length; i++) {
6143 if (tags[i]) {
6144 this.keydown.insertBeforeFirstElement(tags[i]);
6145 return false;
6146 }
6147 }
6148 },
6149 insertAfterLastElement: function (element) {
6150 if (!this.utils.isEndOfElement(element)) {
6151 return;
6152 }
6153
6154 var last = this.core.editor().contents().last();
6155 var $next = (element.tagName === 'FIGCAPTION') ? $(this.keydown.block).parent().next() : $(
6156 this.keydown.block).next();
6157
6158 if ($next.length !== 0) {
6159 return;
6160 }
6161 else if (last.length === 0 && last[0] !== element) {
6162 this.caret.start(last);
6163 return;
6164 }
6165 else {
6166 var node = $(this.opts.emptyHtml);
6167
6168 if (element.tagName === 'FIGCAPTION') {
6169 $(element).parent().after(node);
6170 }
6171 else {
6172 $(element).after(node);
6173 }
6174
6175 this.caret.start(node);
6176 }
6177
6178 },
6179 insertBeforeFirstElement: function (element) {
6180 if (!this.utils.isStartOfElement()) {
6181 return;
6182 }
6183
6184 if (this.core.editor().contents().length > 1 && this.core.editor().contents().first()[0] !== element) {
6185 return;
6186 }
6187
6188 var node = $(this.opts.emptyHtml);
6189 $(element).before(node);
6190 this.caret.start(node);
6191
6192 },
6193 onTab: function (e, key) {
6194 if (!this.opts.tabKey) {
6195 return true;
6196 }
6197
6198 var isList = (this.keydown.block && this.keydown.block.tagName === 'LI');
6199 if (this.utils.isEmpty(this.code.get()) || (!isList && !this.keydown.pre && this.opts.tabAsSpaces === false)) {
6200 return true;
6201 }
6202
6203 e.preventDefault();
6204 this.buffer.set();
6205
6206 var isListStart = (isList && this.utils.isStartOfElement(this.keydown.block));
6207 var node;
6208
6209 if (this.keydown.pre && !e.shiftKey) {
6210 node = (this.opts.preSpaces) ? document.createTextNode(Array(this.opts.preSpaces + 1).join(
6211 '\u00a0')) : document.createTextNode('\t');
6212 this.insert.node(node);
6213 }
6214 else if (this.opts.tabAsSpaces !== false && !isListStart) {
6215 node = document.createTextNode(Array(this.opts.tabAsSpaces + 1).join(
6216 '\u00a0'));
6217 this.insert.node(node);
6218 }
6219 else {
6220 if (e.metaKey && key === 219) {
6221 this.indent.decrease();
6222 }
6223 else if (e.metaKey && key === 221) {
6224 this.indent.increase();
6225 }
6226 else if (!e.shiftKey) {
6227 this.indent.increase();
6228 }
6229 else {
6230 this.indent.decrease();
6231 }
6232 }
6233
6234 return false;
6235 },
6236 setupSelectAll: function (e, key) {
6237 if (this.keydown.ctrl && key === 65) {
6238 this.utils.enableSelectAll();
6239 }
6240 else if (key !== this.keyCode.LEFT_WIN && !this.keydown.ctrl) {
6241 this.utils.disableSelectAll();
6242 }
6243 },
6244 insertNewLine: function (e) {
6245 e.preventDefault();
6246
6247 var node = document.createTextNode('\n');
6248
6249 var sel = this.selection.get();
6250 var range = this.selection.range(sel);
6251
6252 range.deleteContents();
6253 range.insertNode(node);
6254
6255 this.caret.after(node);
6256
6257 return false;
6258 },
6259 insertParagraph: function (e) {
6260 e.preventDefault();
6261
6262 var p = document.createElement('p');
6263 //p.innerHTML = this.opts.invisibleSpace;
6264 p.innerHTML = '<br>';
6265
6266 var sel = this.selection.get();
6267 var range = this.selection.range(sel);
6268
6269 range.deleteContents();
6270 range.insertNode(p);
6271
6272 this.caret.start(p);
6273
6274 return false;
6275 },
6276 insertBreakLine: function (e) {
6277 return this.keydown.insertBreakLineProcessing(e);
6278 },
6279 insertDblBreakLine: function (e) {
6280 return this.keydown.insertBreakLineProcessing(e, true);
6281 },
6282 insertBreakLineProcessing: function (e, dbl) {
6283 e.stopPropagation();
6284
6285 var br1 = document.createElement('br');
6286 this.insert.node(br1);
6287
6288 if (dbl === true) {
6289 var br2 = document.createElement('br');
6290 this.insert.node(br2);
6291 this.caret.after(br2);
6292 }
6293 else {
6294 this.caret.after(br1);
6295 }
6296
6297 return false;
6298
6299 },
6300 wrapToParagraph: function () {
6301 var $current = $(this.keydown.current);
6302 var node = $('<p>').append($current.clone());
6303 $current.replaceWith(node);
6304
6305 var next = $(node).next();
6306 if (typeof (next[0]) !== 'undefined' && next[0].tagName === 'BR') {
6307 next.remove();
6308 }
6309
6310 this.caret.end(node);
6311
6312 },
6313 replaceToParagraph: function (tag) {
6314 var blockElem = this.selection.block();
6315 var $prev = $(blockElem).prev();
6316
6317 var blockHtml = blockElem.innerHTML.replace(/<br\s?\/?>/gi, '');
6318 if (blockElem.tagName === tag && this.utils.isEmpty(blockHtml) && !$(blockElem).hasClass(
6319 'redactor-in')) {
6320 var p = document.createElement('p');
6321 $(blockElem).replaceWith(p);
6322
6323 this.keydown.setCaretToParagraph(p);
6324
6325 return false;
6326 }
6327 else if (blockElem.tagName === 'P') {
6328 $(blockElem).removeAttr('class').removeAttr('style');
6329
6330 // fix #227
6331 if (this.detect.isIe() && this.utils.isEmpty(blockHtml) && this.utils.isInline(
6332 this.keydown.parent)) {
6333 $(blockElem).on('input', $.proxy(function () {
6334 var parent = this.selection.parent();
6335 if (this.utils.isInline(parent)) {
6336 var html = $(parent).html();
6337 $(blockElem).html(html);
6338 this.caret.end(blockElem);
6339 }
6340
6341 $(blockElem).off('keyup');
6342
6343 }, this));
6344 }
6345
6346 return false;
6347 }
6348 else if ($prev.hasClass(this.opts.videoContainerClass)) {
6349 $prev.removeAttr('class');
6350
6351 var p = document.createElement('p');
6352 $prev.replaceWith(p);
6353
6354 this.keydown.setCaretToParagraph(p);
6355
6356 return false;
6357 }
6358 },
6359 setCaretToParagraph: function (p) {
6360 var range = document.createRange();
6361 range.setStart(p, 0);
6362
6363 var textNode = document.createTextNode('\u200B');
6364
6365 range.insertNode(textNode);
6366 range.setStartAfter(textNode);
6367 range.collapse(true);
6368
6369 var sel = window.getSelection();
6370 sel.removeAllRanges();
6371 sel.addRange(range);
6372 },
6373 removeInvisibleSpace: function () {
6374 var $current = $(this.keydown.current);
6375 if ($current.text().search(/^\u200B$/g) === 0) {
6376 $current.remove();
6377 }
6378 },
6379 removeEmptyListInTable: function (e) {
6380 var $current = $(this.keydown.current);
6381 var $parent = $(this.keydown.parent);
6382 var td = $current.closest('td', this.$editor[0]);
6383
6384 if (td.length !== 0 && $current.closest('li',
6385 this.$editor[0]
6386 ) && $parent.children('li').length === 1) {
6387 if (!this.utils.isEmpty($current.text())) {
6388 return;
6389 }
6390
6391 e.preventDefault();
6392
6393 $current.remove();
6394 $parent.remove();
6395
6396 this.caret.start(td);
6397 }
6398 },
6399 removeEmptyLists: function () {
6400 var removeIt = function () {
6401 var html = $.trim(this.innerHTML).replace(/\/t\/n/g, '');
6402 if (html === '') {
6403 $(this).remove();
6404 }
6405 };
6406
6407 this.core.editor().find('li').each(removeIt);
6408 this.core.editor().find('ul, ol').each(removeIt);
6409 },
6410 formatEmpty: function (e) {
6411 var html = $.trim(this.core.editor().html());
6412
6413 if (!this.utils.isEmpty(html)) {
6414 return;
6415 }
6416
6417 e.preventDefault();
6418
6419 if (this.opts.type === 'inline' || this.opts.type === 'pre') {
6420 this.core.editor().html(this.marker.html());
6421 this.selection.restore();
6422 }
6423 else {
6424 var updateHtml = function() {
6425 this.core.editor().html(this.opts.emptyHtml);
6426 this.focus.start();
6427 }.bind(this);
6428
6429 if (Environment !== null && Environment.platform() === 'ios') {
6430 // In iOS Safari the backspace sometimes appears to be triggered twice if the editor
6431 // is completely empty. After debugging for way too much time, and realizing that
6432 // the remote debugger's breakpoints alter the behavior of async callbacks (*), this
6433 // should solve the issue.
6434 //
6435 // (*) Set up a `console.log()` inside a MutationObserver and then make use of the
6436 // `debugger;` statement to halt the execution flow. The observer is executed, but
6437 // the output never appears on the console. Output works if there is no breakpoint.
6438 setTimeout(updateHtml, 50);
6439 }
6440 else {
6441 updateHtml();
6442 }
6443 }
6444
6445 return false;
6446
6447 }
6448 };
6449 },
6450
6451 // =keyup
6452 keyup: function () {
6453 return {
6454 init: function (e) {
6455 if (this.rtePaste) {
6456 return;
6457 }
6458
6459 var key = e.which;
6460 this.keyup.block = this.selection.block();
6461 this.keyup.current = this.selection.current();
6462 this.keyup.parent = this.selection.parent();
6463 this.keyup.lastShiftKey = e.shiftKey;
6464
6465 // callback
6466 var stop = this.core.callback('keyup', e);
6467 if (stop === false) {
6468 e.preventDefault();
6469 return false;
6470 }
6471
6472 // replace a prev figure to paragraph if caret is before image
6473 if (key === this.keyCode.ENTER) {
6474 if (this.keyup.block && this.keyup.block.tagName === 'FIGURE') {
6475 var $prev = $(this.keyup.block).prev();
6476 if ($prev.length !== 0 && $prev[0].tagName === 'FIGURE') {
6477 var $newTag = this.utils.replaceToTag($prev, 'p');
6478 this.caret.start($newTag);
6479 return;
6480 }
6481 }
6482 }
6483
6484 // replace figure to paragraph
6485 if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) {
6486 if (this.utils.isSelectAll()) {
6487 this.focus.start();
6488
6489 return;
6490 }
6491
6492 // if caret before figure - delete image
6493 if (this.keyup.block && this.keydown.block && this.keyup.block.tagName === 'FIGURE' && this.utils.isStartOfElement(
6494 this.keydown.block)) {
6495 e.preventDefault();
6496
6497 this.selection.save();
6498 $(this.keyup.block).find('figcaption').remove();
6499 $(this.keyup.block).find('img').first().remove();
6500 this.utils.replaceToTag(this.keyup.block, 'p');
6501
6502 var $marker = this.marker.find();
6503 $('html, body').animate({scrollTop: $marker.position().top + 20},
6504 500
6505 );
6506
6507 this.selection.restore();
6508 return;
6509 }
6510
6511 // if paragraph does contain only image replace to figure
6512 if (this.keyup.block && this.keyup.block.tagName === 'P') {
6513 var isContainImage = $(this.keyup.block).find('img').length;
6514 var text = $(this.keyup.block).text().replace(/\u200B/g, '');
6515 if (text === '' && isContainImage !== 0) {
6516 this.utils.replaceToTag(this.keyup.block, 'figure');
6517 }
6518 }
6519
6520 // if figure does not contain image - replace to paragraph
6521 if (this.keyup.block && this.keyup.block.tagName === 'FIGURE' && $(this.keyup.block).find(
6522 'img').length === 0) {
6523 this.selection.save();
6524 this.utils.replaceToTag(this.keyup.block, 'p');
6525 this.selection.restore();
6526 }
6527 }
6528 }
6529
6530 };
6531 },
6532
6533 // =lang
6534 lang: function () {
6535 return {
6536 load: function () {
6537 this.opts.curLang = this.opts.langs[this.opts.lang];
6538 },
6539 get: function (name) {
6540 return (typeof this.opts.curLang[name] !== 'undefined') ? this.opts.curLang[name] : '';
6541 }
6542 };
6543 },
6544
6545 // =line
6546 line: function () {
6547 return {
6548 insert: function () {
6549 this.buffer.set();
6550
6551 // insert
6552 this.insert.html(this.line.getLineHtml());
6553
6554 // find
6555 var $hr = this.core.editor().find('#redactor-hr-tmp-id');
6556 $hr.removeAttr('id');
6557
6558 this.core.callback('insertedLine', $hr);
6559
6560 return $hr;
6561 },
6562 getLineHtml: function () {
6563 var html = '<hr id="redactor-hr-tmp-id" />';
6564 if (!this.detect.isFirefox() && this.utils.isEmpty()) {
6565 html += '<p>' + this.opts.emptyHtml + '</p>';
6566 }
6567
6568 return html;
6569 }, // ff only
6570 removeOnBackspace: function (e) {
6571 if (!this.utils.isCollapsed()) {
6572 return;
6573 }
6574
6575 var $block = $(this.selection.block());
6576 if ($block.length === 0 || !this.utils.isStartOfElement($block)) {
6577 return;
6578 }
6579
6580 // if hr is previous element
6581 var $prev = $block.prev();
6582 if ($prev && $prev.length !== 0 && $prev[0].tagName === 'HR') {
6583 e.preventDefault();
6584 $prev.remove();
6585 }
6586 }
6587 };
6588 },
6589
6590 // =link
6591 link: function () {
6592 return {
6593
6594 // public
6595 get: function () {
6596 return $(this.selection.inlines('a'));
6597 },
6598 is: function () {
6599 var nodes = this.selection.nodes();
6600 var $link = $(this.selection.current()).closest('a', this.core.editor()[0]);
6601
6602 return ($link.length === 0 || nodes.length > 1) ? false : $link;
6603 },
6604 unlink: function (e) {
6605 // if call from clickable element
6606 if (typeof e !== 'undefined' && e.preventDefault) {
6607 e.preventDefault();
6608 }
6609
6610 // buffer
6611 this.buffer.set();
6612
6613 var links = this.selection.inlines('a');
6614 if (links.length === 0) {
6615 return;
6616 }
6617
6618 var $links = this.link.replaceLinksToText(links);
6619
6620 this.observe.closeAllTooltip();
6621 this.core.callback('deletedLink', $links);
6622
6623 },
6624 insert: function (link, cleaned) {
6625 var $el = this.link.is();
6626
6627 if (cleaned !== true) {
6628 link = this.link.buildLinkFromObject($el, link);
6629 if (link === false) {
6630 return false;
6631 }
6632 }
6633
6634 // buffer
6635 this.buffer.set();
6636
6637 // callback
6638 link = this.core.callback('beforeInsertingLink', link);
6639
6640 if ($el === false) {
6641 // insert
6642 $el = $('<a />');
6643 $el = this.link.update($el, link);
6644 $el = $(this.insert.node($el));
6645
6646 var $parent = $el.parent();
6647 if (this.utils.isRedactorParent($parent) === false) {
6648 $el.wrap('<p>');
6649 }
6650
6651 // remove unlink wrapper
6652 if ($parent.hasClass('redactor-unlink')) {
6653 $parent.replaceWith(function () {
6654 return $(this).contents();
6655 });
6656 }
6657
6658 this.caret.after($el);
6659 this.core.callback('insertedLink', $el);
6660 }
6661 else {
6662 // update
6663 $el = this.link.update($el, link);
6664 this.caret.after($el);
6665 }
6666
6667 return $el;
6668
6669 },
6670 update: function ($el, link) {
6671 $el.text(link.text);
6672 $el.attr('href', link.url);
6673
6674 this.link.target($el, link.target);
6675
6676 return $el;
6677
6678 },
6679 target: function ($el, target) {
6680 return (target) ? $el.attr('target', '_blank') : $el.removeAttr('target');
6681 },
6682 show: function (e) {
6683 // if call from clickable element
6684 if (typeof e !== 'undefined' && e.preventDefault) {
6685 e.preventDefault();
6686 }
6687
6688 // close tooltip
6689 this.observe.closeAllTooltip();
6690
6691 // is link
6692 var $el = this.link.is();
6693
6694 // build modal
6695 this.link.buildModal($el);
6696
6697 // build link
6698 var link = this.link.buildLinkFromElement($el);
6699
6700 // if link cut & paste inside editor browser added self host to a link
6701 link.url = this.link.removeSelfHostFromUrl(link.url);
6702
6703 // new tab target
6704 if (this.opts.linkNewTab && !$el) {
6705 link.target = true;
6706 }
6707
6708 // set modal values
6709 this.link.setModalValues(link);
6710
6711 // show modal
6712 this.modal.show();
6713
6714 // focus
6715 if (this.detect.isDesktop()) {
6716 $('#redactor-link-url').focus();
6717 }
6718 },
6719
6720 // private
6721 setModalValues: function (link) {
6722 $('#redactor-link-blank').prop('checked', link.target);
6723 $('#redactor-link-url').val(link.url);
6724 $('#redactor-link-url-text').val(link.text);
6725 },
6726 buildModal: function ($el) {
6727 this.modal.load('link',
6728 this.lang.get(($el === false) ? 'link-insert' : 'link-edit'),
6729 600
6730 );
6731
6732 // button insert
6733 var $btn = this.modal.getActionButton();
6734 $btn.text(this.lang.get(($el === false) ? 'insert' : 'save')).on('click',
6735 $.proxy(this.link.callback, this)
6736 );
6737
6738 },
6739 callback: function () {
6740 // build link
6741 var link = this.link.buildLinkFromModal();
6742 if (link === false) {
6743 return false;
6744 }
6745
6746 // close
6747 this.modal.close();
6748
6749 // insert or update
6750 this.link.insert(link, true);
6751 },
6752 cleanUrl: function (url) {
6753 return (typeof url === 'undefined') ? '' : $.trim(url.replace(/[^\W\w\D\d+&\'@#/%?=~_|!:,.;\(\)]/gi,
6754 ''
6755 ));
6756 },
6757 cleanText: function (text) {
6758 return (typeof text === 'undefined') ? '' : $.trim(text.replace(/(<([^>]+)>)/gi,
6759 ''
6760 ));
6761 },
6762 getText: function (link) {
6763 return (link.text === '' && link.url !== '') ? this.link.truncateUrl(link.url.replace(/<|>/g,
6764 ''
6765 )) : link.text;
6766 },
6767 isUrl: function (url) {
6768 var reUrl = new RegExp(
6769 '^((https?|ftp):\\/\\/)?(([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$',
6770 'i'
6771 );
6772
6773 return (reUrl.test(url)) ? url : false;
6774 },
6775 isMailto: function (url) {
6776 return (url.search('@') !== -1 && /(http|ftp|https):\/\//i.test(url) === false);
6777 },
6778 isEmpty: function (link) {
6779 return (link.url === '' || (link.text === '' && link.url === ''));
6780 },
6781 truncateUrl: function (url) {
6782 return (url.length > this.opts.linkSize) ? url.substring(0,
6783 this.opts.linkSize
6784 ) + '...' : url;
6785 },
6786 parse: function (link) {
6787 // mailto
6788 if (this.link.isMailto(link.url)) {
6789 link.url = 'mailto:' + link.url.replace('mailto:', '');
6790 }
6791 // url
6792 else if (link.url.search('#') !== 0) {
6793 if (this.opts.linkValidation) {
6794 link.url = (this.link.isUrl(link.url)) ? 'http://' + link.url.replace(/(ftp|https?):\/\//gi,
6795 ''
6796 ) : link.url;
6797 }
6798 }
6799
6800 // empty url or text or isn't url
6801 return (this.link.isEmpty(link) || link.url === false) ? false : link;
6802
6803 },
6804 buildLinkFromModal: function () {
6805 var link = {};
6806
6807 // url
6808 link.url = this.link.cleanUrl($('#redactor-link-url').val());
6809
6810 // text
6811 link.text = this.link.cleanText($('#redactor-link-url-text').val());
6812 link.text = this.link.getText(link);
6813
6814 // target
6815 link.target = ($('#redactor-link-blank').prop('checked')) ? true : false;
6816
6817 // parse
6818 return this.link.parse(link);
6819
6820 },
6821 buildLinkFromObject: function ($el, link) {
6822 // url
6823 link.url = this.link.cleanUrl(link.url);
6824
6825 // text
6826 link.text = (typeof link.text === 'undefined' && this.selection.is()) ? this.selection.text() : this.link.cleanText(
6827 link.text);
6828 link.text = this.link.getText(link);
6829
6830 // target
6831 link.target = ($el === false) ? link.target : this.link.buildTarget($el);
6832
6833 // parse
6834 return this.link.parse(link);
6835
6836 },
6837 buildLinkFromElement: function ($el) {
6838 var link = {
6839 url: '',
6840 text: (this.selection.is()) ? this.selection.text() : '',
6841 target: false
6842 };
6843
6844 if ($el !== false) {
6845 link.url = $el.attr('href');
6846 link.text = $el.text();
6847 link.target = this.link.buildTarget($el);
6848 }
6849
6850 return link;
6851 },
6852 buildTarget: function ($el) {
6853 return (typeof $el.attr('target') !== 'undefined' && $el.attr('target') === '_blank') ? true : false;
6854 },
6855 removeSelfHostFromUrl: function (url) {
6856 var href = self.location.href.replace('#', '').replace(/\/$/i, '');
6857 return url.replace(/^\/\#/, '#').replace(href, '').replace('mailto:', '');
6858 },
6859 replaceLinksToText: function (links) {
6860 var $first;
6861 var $links = $.each(links, function (i, s) {
6862 var $el = $(s);
6863 var $unlinked = $('<span class="redactor-unlink" />').append($el.contents());
6864 $el.replaceWith($unlinked);
6865
6866 if (i === 0) {
6867 $first = $unlinked;
6868 }
6869
6870 return $el;
6871 });
6872
6873 // set caret after unlinked node
6874 if (links.length === 1 && this.selection.isCollapsed()) {
6875 this.caret.after($first);
6876 }
6877
6878 return $links;
6879 }
6880 };
6881 },
6882
6883 // =linkify -- UNSUPPORTED MODULE
6884 linkify: function () {
6885 return {
6886 isKey: function () {},
6887 isLink: function () {},
6888 isFiltered: function () {},
6889 handler: function () {},
6890 format: function () {},
6891 convertVideoLinks: function () {},
6892 convertImages: function () {},
6893 convertLinks: function () {}
6894 };
6895 },
6896
6897 // =list
6898 list: function () {
6899 return {
6900 toggle: function (type) {
6901 if (this.utils.inBlocks(['table', 'td', 'th', 'tr'])) {
6902 return;
6903 }
6904
6905 type = (type === 'orderedlist') ? 'ol' : type;
6906 type = (type === 'unorderedlist') ? 'ul' : type;
6907
6908 type = type.toLowerCase();
6909
6910 this.buffer.set();
6911 this.selection.save();
6912
6913 var nodes = this.list._getBlocks();
6914 var block = this.selection.block();
6915 var $list = $(block).parents('ul, ol').last();
6916 if (nodes.length === 0 && $list.length !== 0) {
6917 nodes = [$list.get(0)];
6918 }
6919
6920 nodes = (this.list._isUnformat(type, nodes)) ? this.list._unformat(type,
6921 nodes
6922 ) : this.list._format(type, nodes);
6923
6924 this.selection.restore();
6925
6926 return nodes;
6927 },
6928 get: function () {
6929 var current = this.selection.current();
6930 var $list = $(current).closest('ul, ol', this.core.editor()[0]);
6931
6932 return ($list.length === 0) ? false : $list;
6933 },
6934 combineAfterAndBefore: function (block) {
6935 var $prev = $(block).prev();
6936 var $next = $(block).next();
6937 var isEmptyBlock = (block && block.tagName === 'P' && (block.innerHTML === '<br>' || block.innerHTML === ''));
6938 var isBlockWrapped = ($prev.closest('ol, ul',
6939 this.core.editor()[0]
6940 ).length === 1 && $next.closest(
6941 'ol, ul',
6942 this.core.editor()[0]
6943 ).length === 1);
6944
6945 if (isEmptyBlock && isBlockWrapped) {
6946 $prev.children('li').last().append(this.marker.get());
6947 $prev.append($next.contents());
6948 this.selection.restore();
6949
6950 return true;
6951 }
6952
6953 return false;
6954
6955 },
6956 _getBlocks: function () {
6957 var finalBlocks = [];
6958 var blocks = this.selection.blocks();
6959 for (var i = 0; i < blocks.length; i++) {
6960 var $el = $(blocks[i]);
6961 var isFirst = ($el.parent().hasClass('redactor-in'));
6962
6963 if (isFirst) finalBlocks.push(blocks[i]);
6964 }
6965
6966 return finalBlocks;
6967 },
6968 _isUnformat: function (type, nodes) {
6969 var countLists = 0;
6970 for (var i = 0; i < nodes.length; i++) {
6971 if (nodes[i].nodeType !== 3) {
6972 var tag = nodes[i].tagName.toLowerCase();
6973 if (tag === type || tag === 'figure') {
6974 countLists++;
6975 }
6976 }
6977 }
6978
6979 return (countLists === nodes.length);
6980 },
6981 _uniteBlocks: function (nodes, tags) {
6982 var z = 0;
6983 var blocks = {0: []};
6984 var lastcell = false;
6985 for (var i = 0; i < nodes.length; i++) {
6986 var $node = $(nodes[i]);
6987 var $cell = $node.closest('th, td');
6988
6989 if ($cell.length !== 0) {
6990 if ($cell.get(0) !== lastcell) {
6991 // create block
6992 z++;
6993 blocks[z] = [];
6994 }
6995
6996 if (this.list._isUniteBlock(nodes[i], tags)) {
6997 blocks[z].push(nodes[i]);
6998 }
6999 }
7000 else {
7001 if (this.list._isUniteBlock(nodes[i], tags)) {
7002 blocks[z].push(nodes[i]);
7003 }
7004 else {
7005 // create block
7006 z++;
7007 blocks[z] = [];
7008 }
7009 }
7010
7011 lastcell = $cell.get();
7012 }
7013
7014 return blocks;
7015 },
7016 _isUniteBlock: function (node, tags) {
7017 return (node.nodeType === 3 || tags.indexOf(node.tagName.toLowerCase()) !== -1);
7018 },
7019 _createList: function (type, blocks, key) {
7020 var last = blocks[blocks.length - 1];
7021 var $last = $(last);
7022 var $list = $('<' + type + '>');
7023 $last.after($list);
7024
7025 return $list;
7026 },
7027 _createListItem: function (item) {
7028 var $item = $('<li>');
7029 if (item.nodeType === 3) {
7030 $item.append(item);
7031 }
7032 else {
7033 var $el = $(item);
7034 $item.append($el.contents());
7035 $el.remove();
7036 }
7037
7038 return $item;
7039 },
7040 _format: function (type, nodes) {
7041 var tags = [
7042 'p',
7043 'div',
7044 'blockquote',
7045 'pre',
7046 'h1',
7047 'h2',
7048 'h3',
7049 'h4',
7050 'h5',
7051 'h6',
7052 'ul',
7053 'ol'
7054 ];
7055 var blocks = this.list._uniteBlocks(nodes, tags);
7056 var lists = [];
7057
7058 for (var key in blocks) {
7059 var items = blocks[key];
7060 var $list = this.list._createList(type, blocks[key]);
7061
7062 for (var i = 0; i < items.length; i++) {
7063 var $item;
7064
7065 // lists
7066 if (items[i].nodeType !== 3 && (items[i].tagName === 'UL' || items[i].tagName === 'OL')) {
7067 $item = $(items[i]).contents();
7068 $list.append($item);
7069 }
7070 // other blocks or texts
7071 else {
7072 $item = this.list._createListItem(items[i]);
7073 //this.utils.normalizeTextNodes($item);
7074 $list.append($item);
7075 }
7076 }
7077
7078 lists.push($list.get(0));
7079 }
7080
7081 return lists;
7082 },
7083 _unformat: function (type, nodes) {
7084
7085 if (nodes.length === 1) {
7086 // one list
7087 var $list = $(nodes[0]);
7088 var $items = $list.find('li');
7089
7090 var selectedItems = this.selection.blocks(['li']);
7091 var block = this.selection.block();
7092 var $li = $(block).closest('li');
7093 if (selectedItems.length === 0 && $li.length !== 0) {
7094 selectedItems = [$li.get(0)];
7095 }
7096
7097 // 1) entire
7098 if (selectedItems.length === $items.length) {
7099 return this.list._unformatEntire(nodes[0]);
7100 }
7101
7102 var pos = this.list._getItemsPosition($items, selectedItems);
7103
7104 // 2) top
7105 if (pos === 'Top') {
7106 return this.list._unformatAtSide('before',
7107 selectedItems,
7108 $list
7109 );
7110 }
7111
7112 // 3) bottom
7113 else if (pos === 'Bottom') {
7114 selectedItems.reverse();
7115 return this.list._unformatAtSide('after', selectedItems, $list);
7116 }
7117
7118 // 4) middle
7119 else if (pos === 'Middle') {
7120 var $last = $(selectedItems[selectedItems.length - 1]);
7121
7122 var ci = false;
7123
7124 var $parent = false;
7125 var $secondList = $('<' + $list.get(0).tagName.toLowerCase() + '>');
7126 $items.each(function (i, node) {
7127 if (ci) {
7128 var $node = $(node);
7129 var $childList = ($node.children('ul, ol').length !== 0);
7130
7131 if ($node.closest('.redactor-split-item').length === 0 && ($parent === false || $node.closest(
7132 $parent).length === 0)) {
7133 $node.addClass('redactor-split-item');
7134 }
7135
7136 $parent = $node;
7137
7138 }
7139
7140 if (node === $last.get(0)) {
7141 ci = true;
7142 }
7143 });
7144
7145 $items.filter('.redactor-split-item').each(function (i, node) {
7146 var $node = $(node);
7147 $node.removeClass('redactor-split-item');
7148 $secondList.append(node);
7149 });
7150
7151 $list.after($secondList);
7152
7153 selectedItems.reverse();
7154 for (var i = 0; i < selectedItems.length; i++) {
7155 var $item = $(selectedItems[i]);
7156 var $container = this.list._createUnformatContainer(
7157 $item);
7158
7159 $list.after($container);
7160 $container.find('ul, ol').remove();
7161 $item.remove();
7162 }
7163
7164 return;
7165 }
7166
7167 }
7168 else {
7169 // unformat all
7170 for (var i = 0; i < nodes.length; i++) {
7171 if (nodes[i].nodeType !== 3 && nodes[i].tagName.toLowerCase() === type) {
7172 this.list._unformatEntire(nodes[i]);
7173 }
7174 }
7175 }
7176 },
7177 _unformatEntire: function (list) {
7178 var $list = $(list);
7179 var $items = $list.find('li');
7180 $items.each(function (i, node) {
7181 var $item = $(node);
7182 var $container = this.list._createUnformatContainer($item);
7183
7184 $item.remove();
7185 $list.before($container);
7186
7187 }.bind(this));
7188
7189 $list.remove();
7190 },
7191 _unformatAtSide: function (type, selectedItems, $list) {
7192 for (var i = 0; i < selectedItems.length; i++) {
7193 var $item = $(selectedItems[i]);
7194 var $container = this.list._createUnformatContainer($item);
7195
7196 $list[type]($container);
7197
7198 var $innerLists = $container.find('ul, ol').first();
7199 $item.append($innerLists);
7200
7201 $innerLists.each(function (i, node) {
7202 var $node = $(node);
7203 var $parent = $node.closest('li');
7204
7205 if ($parent.get(0) === selectedItems[i]) {
7206 $node.unwrap();
7207 $parent.addClass('r-unwrapped');
7208 }
7209
7210 });
7211
7212 if (this.utils.isEmpty($item.html())) $item.remove();
7213 }
7214
7215 // clear empty
7216 $list.find('.r-unwrapped').each(function (node) {
7217 var $node = $(node);
7218 if ($node.html().trim() === '') {
7219 $node.remove();
7220 }
7221 else {
7222 $node.removeClass('r-unwrapped');
7223 }
7224 });
7225 },
7226 _getItemsPosition: function ($items, selectedItems) {
7227 var pos = 'Middle';
7228
7229 var sFirst = selectedItems[0];
7230 var sLast = selectedItems[selectedItems.length - 1];
7231
7232 var first = $items.first().get(0);
7233 var last = $items.last().get(0);
7234
7235 if (first === sFirst && last !== sLast) {
7236 pos = 'Top';
7237 }
7238 else if (first !== sFirst && last === sLast) {
7239 pos = 'Bottom';
7240 }
7241
7242 return pos;
7243 },
7244 _createUnformatContainer: function ($item) {
7245 var $container = $('<p>');
7246 $container.append($item.contents());
7247
7248 return $container;
7249 }
7250 };
7251 },
7252
7253 // =marker
7254 marker: function () {
7255 return {
7256
7257 // public
7258 get: function (num) {
7259 num = (typeof num === 'undefined') ? 1 : num;
7260
7261 var marker = document.createElement('span');
7262
7263 marker.id = 'selection-marker-' + num;
7264 marker.className = 'redactor-selection-marker';
7265 marker.innerHTML = this.opts.invisibleSpace;
7266
7267 return marker;
7268 },
7269 html: function (num) {
7270 return this.utils.getOuterHtml(this.marker.get(num));
7271 },
7272 find: function (num) {
7273 num = (typeof num === 'undefined') ? 1 : num;
7274
7275 return this.core.editor().find('span#selection-marker-' + num);
7276 },
7277 insert: function () {
7278 var sel = this.selection.get();
7279 var range = this.selection.range(sel);
7280
7281 this.marker.insertNode(range, this.marker.get(1), true);
7282 if (range && range.collapsed === false) {
7283 this.marker.insertNode(range, this.marker.get(2), false);
7284 }
7285
7286 },
7287 remove: function () {
7288 this.core.editor().find('.redactor-selection-marker').each(this.marker.iterateRemove);
7289 },
7290
7291 // private
7292 insertNode: function (range, node, collapse) {
7293 var parent = this.selection.parent();
7294 if (range === null || $(parent).closest('.redactor-in').length === 0) {
7295 return;
7296 }
7297
7298 range = range.cloneRange();
7299
7300 try {
7301 range.collapse(collapse);
7302 range.insertNode(node);
7303 }
7304 catch (e) {
7305 this.focus.start();
7306 }
7307 },
7308 iterateRemove: function (i, el) {
7309 var $el = $(el);
7310 var text = $el.text().replace(/\u200B/g, '');
7311 var parent = $el.parent()[0];
7312
7313 if (text === '') $el.remove(); else $el.replaceWith(function () { return $(this).contents(); });
7314
7315 // if (parent && parent.normalize) parent.normalize();
7316 }
7317 };
7318 },
7319
7320 // =modal
7321 modal: function () {
7322 return {
7323 callbacks: {},
7324 templates: function () {
7325 this.opts.modal = {
7326 'image-edit': '',
7327
7328 'image': '',
7329
7330 'file': '',
7331
7332 'link': String() + '<div class="redactor-modal-tab" data-title="General">' + '<section>' + '<label>URL</label>' + '<input type="url" id="redactor-link-url" aria-label="URL" />' + '</section>' + '<section>' + '<label>' + this.lang.get(
7333 'text') + '</label>' + '<input type="text" id="redactor-link-url-text" aria-label="' + this.lang.get(
7334 'text') + '" />' + '</section>' + '<section>' + '<label class="checkbox"><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get(
7335 'link-in-new-tab') + '</label>' + '</section>' + '<section>' + '<button id="redactor-modal-button-action">' + this.lang.get(
7336 'insert') + '</button>' + '<button id="redactor-modal-button-cancel">' + this.lang.get(
7337 'cancel') + '</button>' + '</section>' + '</div>'
7338 };
7339
7340 $.extend(this.opts, this.opts.modal);
7341
7342 },
7343 addCallback: function (name, callback) {
7344 this.modal.callbacks[name] = callback;
7345 },
7346 addTemplate: function (name, template) {
7347 this.opts.modal[name] = template;
7348 },
7349 getTemplate: function (name) {
7350 return this.opts.modal[name];
7351 },
7352 getModal: function () {
7353 return this.$modalBody;
7354 },
7355 getActionButton: function () {
7356 return this.$modalBody.find('#redactor-modal-button-action');
7357 },
7358 getCancelButton: function () {
7359 return this.$modalBody.find('#redactor-modal-button-cancel');
7360 },
7361 getDeleteButton: function () {
7362 return this.$modalBody.find('#redactor-modal-button-delete');
7363 },
7364 load: function () { /* WoltLabModal.js */ },
7365 show: function () { /* WoltLabModal.js */ },
7366 buildWidth: function () { },
7367 buildTabber: function () {},
7368 showTab: function () {},
7369 setTitle: function () { /* WoltLabModal.js */ },
7370 setContent: function () {
7371 this.$modalBody.html(this.modal.getTemplate(this.modal.templateName));
7372
7373 this.modal.getCancelButton().on('mousedown', $.proxy(this.modal.close, this));
7374 },
7375 setDraggable: function () {},
7376 setEnter: function () {},
7377 build: function () {
7378 this.modal.buildOverlay();
7379
7380 this.$modalBox = $('<div id="redactor-modal-box"/>').hide();
7381 this.$modal = $('<div id="redactor-modal" role="dialog" />');
7382 this.$modalHeader = $('<div id="redactor-modal-header" />');
7383 this.$modalClose = $(
7384 '<button type="button" id="redactor-modal-close" aria-label="' + this.lang.get(
7385 'close') + '" />').html('&times;');
7386 this.$modalBody = $('<div id="redactor-modal-body" />');
7387
7388 this.$modal.append(this.$modalHeader);
7389 this.$modal.append(this.$modalBody);
7390 this.$modal.append(this.$modalClose);
7391 this.$modalBox.append(this.$modal);
7392 this.$modalBox.appendTo(document.body);
7393
7394 },
7395 buildOverlay: function () {
7396 this.$modalOverlay = $('<div id="redactor-modal-overlay">').hide();
7397 $('body').prepend(this.$modalOverlay);
7398 },
7399 enableEvents: function () {},
7400 disableEvents: function () {},
7401 closeHandler: function () {},
7402 close: function () { /* WoltLabModal.js */ }
7403 };
7404 },
7405
7406 // =observe
7407 observe: function () {
7408 return {
7409 load: function () {
7410 if (typeof this.opts.destroyed !== 'undefined') {
7411 return;
7412 }
7413
7414 this.observe.links();
7415 this.observe.images();
7416
7417 },
7418 isCurrent: function ($el, $current) {
7419 if (typeof $current === 'undefined') {
7420 $current = $(this.selection.current());
7421 }
7422
7423 return $current.is($el) || $current.parents($el).length > 0;
7424 },
7425 toolbar: function () {
7426 this.observe.buttons();
7427 this.observe.dropdowns();
7428 },
7429 buttons: function (e, btnName) {
7430 var current = this.selection.current();
7431 var parent = this.selection.parent();
7432
7433 if (e !== false) {
7434 this.button.setInactiveAll();
7435 }
7436 else {
7437 this.button.setInactiveAll(btnName);
7438 }
7439
7440 if (e === false && btnName !== 'html') {
7441 if ($.inArray(btnName, this.opts.activeButtons) !== -1) {
7442 this.button.toggleActive(btnName);
7443 }
7444 return;
7445 }
7446
7447 if (!this.utils.isRedactorParent(current)) {
7448 return;
7449 }
7450
7451 // disable line
7452 if (this.core.editor().css('display') !== 'none') {
7453 if (this.utils.isCurrentOrParentHeader() || this.utils.isCurrentOrParent(
7454 ['table', 'pre', 'blockquote', 'li'])) {
7455 this.button.disable('horizontalrule');
7456 }
7457 else {
7458 this.button.enable('horizontalrule');
7459 }
7460 }
7461
7462 $.each(this.opts.activeButtonsStates, $.proxy(function (key, value) {
7463 var parentEl = $(parent).closest(key, this.$editor[0]);
7464 var currentEl = $(current).closest(key, this.$editor[0]);
7465
7466 if (parentEl.length !== 0 && !this.utils.isRedactorParent(parentEl)) {
7467 return;
7468 }
7469
7470 if (!this.utils.isRedactorParent(currentEl)) {
7471 return;
7472 }
7473
7474 if (parentEl.length !== 0 || currentEl.closest(key,
7475 this.$editor[0]
7476 ).length !== 0) {
7477 this.button.setActive(value);
7478 }
7479
7480 }, this));
7481
7482 },
7483 dropdowns: function () {
7484 var finded = $('<div />').html(this.selection.html()).find('a').length;
7485 var $current = $(this.selection.current());
7486 var isRedactor = this.utils.isRedactorParent($current);
7487
7488 $.each(this.opts.observe.dropdowns, $.proxy(function (key, value) {
7489 var observe = value.observe, element = observe.element,
7490 $item = value.item,
7491 inValues = typeof observe['in'] !== 'undefined' ? observe['in'] : false,
7492 outValues = typeof observe.out !== 'undefined' ? observe.out : false;
7493
7494 if (($current.closest(element).length > 0 && isRedactor) || (element === 'a' && finded !== 0)) {
7495 this.observe.setDropdownProperties($item, inValues, outValues);
7496 }
7497 else {
7498 this.observe.setDropdownProperties($item, outValues, inValues);
7499 }
7500
7501 }, this));
7502 },
7503 setDropdownProperties: function ($item, addProperties, deleteProperties) {
7504 if (deleteProperties && typeof deleteProperties.attr !== 'undefined') {
7505 this.observe.setDropdownAttr($item, deleteProperties.attr, true);
7506 }
7507
7508 if (typeof addProperties.attr !== 'undefined') {
7509 this.observe.setDropdownAttr($item, addProperties.attr);
7510 }
7511
7512 if (typeof addProperties.title !== 'undefined') {
7513 $item.find('span').text(addProperties.title);
7514 }
7515 },
7516 setDropdownAttr: function ($item, properties, isDelete) {
7517 $.each(properties, function (key, value) {
7518 if (key === 'class') {
7519 if (!isDelete) {
7520 $item.addClass(value);
7521 }
7522 else {
7523 $item.removeClass(value);
7524 }
7525 }
7526 else {
7527 if (!isDelete) {
7528 $item.attr(key, value);
7529 }
7530 else {
7531 $item.removeAttr(key);
7532 }
7533 }
7534 });
7535 },
7536 addDropdown: function ($item, btnName, btnObject) {
7537 if (typeof btnObject.observe === 'undefined') {
7538 return;
7539 }
7540
7541 btnObject.item = $item;
7542
7543 this.opts.observe.dropdowns.push(btnObject);
7544 },
7545 images: function () {
7546 if (this.opts.imageEditable) {
7547 this.core.editor().addClass('redactor-layer-img-edit');
7548 this.core.editor().find('img').each($.proxy(function (i, img) {
7549 var $img = $(img);
7550
7551 // IE fix (when we clicked on an image and then press backspace IE does goes to image's url)
7552 $img.closest('a', this.$editor[0]).on('click',
7553 function (e) { e.preventDefault(); }
7554 );
7555
7556 this.image.setEditable($img);
7557
7558 }, this));
7559 }
7560 },
7561 links: function () {
7562 if (this.opts.linkTooltip) {
7563 this.core.editor().find('a').each($.proxy(function (i, s) {
7564 var $link = $(s);
7565 if ($link.data('cached') !== true) {
7566 $link.data('cached', true);
7567 $link.on(
7568 'touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7569 $.proxy(this.observe.showTooltip, this)
7570 );
7571 }
7572
7573 }, this));
7574 }
7575 },
7576 getTooltipPosition: function ($link) {
7577 return $link.offset();
7578 },
7579 showTooltip: function (e) {
7580 var $el = $(e.target);
7581
7582 if ($el[0].tagName === 'IMG') {
7583 return;
7584 }
7585
7586 if ($el[0].tagName !== 'A') {
7587 $el = $el.closest('a', this.$editor[0]);
7588 }
7589
7590 if ($el[0].tagName !== 'A') {
7591 return;
7592 }
7593
7594 var $link = $el;
7595
7596 var pos = this.observe.getTooltipPosition($link);
7597 var tooltip = $('<span class="redactor-link-tooltip"></span>');
7598
7599 var href = $link.attr('href');
7600 if (href === undefined) {
7601 href = '';
7602 }
7603
7604 if (href.length > 24) {
7605 href = href.substring(0, 24) + '...';
7606 }
7607
7608 var aLink = $('<a href="' + $link.attr('href') + '" target="_blank" />').html(
7609 href).addClass('redactor-link-tooltip-action');
7610 var aEdit = $('<a href="#" />').html(this.lang.get('edit')).on('click',
7611 $.proxy(this.link.show, this)
7612 ).addClass('redactor-link-tooltip-action');
7613 var aUnlink = $('<a href="#" />').html(this.lang.get('unlink')).on('click',
7614 $.proxy(this.link.unlink, this)
7615 ).addClass('redactor-link-tooltip-action');
7616
7617 tooltip.append(aLink).append(' | ').append(aEdit).append(' | ').append(aUnlink);
7618
7619 var lineHeight = parseInt($link.css('line-height'), 10);
7620 var lineClicked = Math.ceil((e.pageY - pos.top) / lineHeight);
7621 var top = pos.top + lineClicked * lineHeight;
7622
7623 tooltip.css({
7624 top: top + 'px',
7625 left: pos.left + 'px'
7626 });
7627
7628 $('.redactor-link-tooltip').remove();
7629 $('body').append(tooltip);
7630
7631 this.core.editor().on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7632 $.proxy(this.observe.closeTooltip, this)
7633 );
7634 $(document).on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7635 $.proxy(this.observe.closeTooltip, this)
7636 );
7637 },
7638 closeAllTooltip: function () {
7639 $('.redactor-link-tooltip').remove();
7640 },
7641 closeTooltip: function (e) {
7642 e = e.originalEvent || e;
7643
7644 var target = e.target;
7645 var $parent = $(target).closest('a', this.$editor[0]);
7646 if ($parent.length !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A') {
7647 return;
7648 }
7649 else if ((target.tagName === 'A' && this.utils.isRedactorParent(target)) || $(
7650 target).hasClass('redactor-link-tooltip-action')) {
7651 return;
7652 }
7653
7654 this.observe.closeAllTooltip();
7655
7656 this.core.editor().off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7657 $.proxy(this.observe.closeTooltip, this)
7658 );
7659 $(document).off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7660 $.proxy(this.observe.closeTooltip, this)
7661 );
7662 }
7663
7664 };
7665 },
7666
7667 // =offset
7668 offset: function () {
7669 return {
7670 get: function (node) {
7671 var cloned = this.offset.clone(node);
7672 if (cloned === false) {
7673 return 0;
7674 }
7675
7676 var div = document.createElement('div');
7677 div.appendChild(cloned.cloneContents());
7678 div.innerHTML = div.innerHTML.replace(/<img(.*?[^>])>$/gi, 'i');
7679
7680 var text = $.trim($(div).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
7681 ''
7682 );
7683
7684 return text.length;
7685
7686 },
7687 clone: function (node) {
7688 var sel = this.selection.get();
7689 var range = this.selection.range(sel);
7690
7691 if (range === null && typeof node === 'undefined') {
7692 return false;
7693 }
7694
7695 node = (typeof node === 'undefined') ? this.$editor : node;
7696 if (node === false) {
7697 return false;
7698 }
7699
7700 node = node[0] || node;
7701
7702 var cloned = range.cloneRange();
7703 cloned.selectNodeContents(node);
7704 cloned.setEnd(range.endContainer, range.endOffset);
7705
7706 return cloned;
7707 },
7708 set: function (start, end) {
7709 end = (typeof end === 'undefined') ? start : end;
7710
7711 if (!this.focus.is()) {
7712 this.focus.start();
7713 }
7714
7715 var sel = this.selection.get();
7716 var range = this.selection.range(sel);
7717 var node, offset = 0;
7718 var walker = document.createTreeWalker(this.$editor[0],
7719 NodeFilter.SHOW_TEXT,
7720 null,
7721 null
7722 );
7723
7724 while ((node = walker.nextNode()) !== null) {
7725 offset += node.nodeValue.length;
7726 if (offset > start) {
7727 range.setStart(node, node.nodeValue.length + start - offset);
7728 start = Infinity;
7729 }
7730
7731 if (offset >= end) {
7732 range.setEnd(node, node.nodeValue.length + end - offset);
7733 break;
7734 }
7735 }
7736
7737 range.collapse(false);
7738 this.selection.update(sel, range);
7739 }
7740 };
7741 },
7742
7743 // =paragraphize
7744 paragraphize: function () {
7745 return {
7746 load: function (html) {
7747 if (this.opts.paragraphize === false || this.opts.type === 'inline' || this.opts.type === 'pre') {
7748 return html;
7749 }
7750
7751 if (html === '' || html === '<p></p>') {
7752 return this.opts.emptyHtml;
7753 }
7754
7755 html = html + '\n';
7756
7757 this.paragraphize.safes = [];
7758 this.paragraphize.z = 0;
7759
7760 // before
7761 html = html.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>');
7762 html = html.replace(/<\/pre>/gi, '</pre>\n\n');
7763 html = html.replace(/<p>\s<br><\/p>/gi, '<p></p>');
7764
7765 html = this.paragraphize.getSafes(html);
7766
7767 html = html.replace('<br>', '\n');
7768 html = this.paragraphize.convert(html);
7769
7770 html = this.paragraphize.clear(html);
7771 html = this.paragraphize.restoreSafes(html);
7772
7773 // after
7774 html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts.paragraphizeBlocks.join(
7775 '|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');
7776
7777 return $.trim(html);
7778 },
7779 getSafes: function (html) {
7780 var $div = $('<div />').append(html);
7781
7782 // remove paragraphs in blockquotes
7783 $div.find('blockquote p').replaceWith(function () {
7784 return $(this).append('<br />').contents();
7785 });
7786
7787 $div.find(this.opts.paragraphizeBlocks.join(', ')).each($.proxy(function (i, s) {
7788 this.paragraphize.z++;
7789 this.paragraphize.safes[this.paragraphize.z] = s.outerHTML;
7790
7791 return $(s).replaceWith('\n#####replace' + this.paragraphize.z + '#####\n\n');
7792
7793 }, this));
7794
7795 // deal with redactor selection markers
7796 $div.find('span.redactor-selection-marker').each($.proxy(function (i, s) {
7797 this.paragraphize.z++;
7798 this.paragraphize.safes[this.paragraphize.z] = s.outerHTML;
7799
7800 return $(s).replaceWith('\n#####replace' + this.paragraphize.z + '#####\n\n');
7801 }, this));
7802
7803 return $div.html();
7804 },
7805 restoreSafes: function (html) {
7806 $.each(this.paragraphize.safes, function (i, s) {
7807 s = (typeof s !== 'undefined') ? s.replace(/\$/g, '&#36;') : s;
7808 html = html.replace('#####replace' + i + '#####', s);
7809
7810 });
7811
7812 return html;
7813 },
7814 convert: function (html) {
7815 html = html.replace(/\r\n/g, 'xparagraphmarkerz');
7816 html = html.replace(/\n/g, 'xparagraphmarkerz');
7817 html = html.replace(/\r/g, 'xparagraphmarkerz');
7818
7819 var re1 = /\s+/g;
7820 html = html.replace(re1, ' ');
7821 html = $.trim(html);
7822
7823 var re2 = /xparagraphmarkerzxparagraphmarkerz/gi;
7824 html = html.replace(re2, '</p><p>');
7825
7826 var re3 = /xparagraphmarkerz/gi;
7827 html = html.replace(re3, '<br>');
7828
7829 html = '<p>' + html + '</p>';
7830
7831 html = html.replace('<p></p>', '');
7832 html = html.replace('\r\n\r\n', '');
7833 html = html.replace(/<\/p><p>/g, '</p>\r\n\r\n<p>');
7834 html = html.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7835 html = html.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7836 html = html.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7837 html = html.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7838 html = html.replace(/<p>&nbsp;<\/p>/gi, '');
7839 html = html.replace(/<p>\s?<br>&nbsp;<\/p>/gi, '');
7840 html = html.replace(/<p>\s?<br>/gi, '<p>');
7841
7842 return html;
7843 },
7844 clear: function (html) {
7845
7846 html = html.replace(
7847 /<p>(.*?)#####replace(.*?)#####\s?<\/p>/gi,
7848 '<p>$1</p>#####replace$2#####'
7849 );
7850 html = html.replace(/(<br\s?\/?>){2,}<\/p>/gi, '</p>');
7851
7852 html = html.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>');
7853 html = html.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>');
7854 html = html.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>');
7855 html = html.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>');
7856
7857 html = html.replace(new RegExp('<p><p ', 'gi'), '<p ');
7858 html = html.replace(new RegExp('<p><p>', 'gi'), '<p>');
7859 html = html.replace(new RegExp('</p></p>', 'gi'), '</p>');
7860 html = html.replace(new RegExp('<p>\\s?</p>', 'gi'), '');
7861 html = html.replace(new RegExp('\n</p>', 'gi'), '</p>');
7862 html = html.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>');
7863 html = html.replace(new RegExp('<p>\t*</p>', 'gi'), '');
7864
7865 return html;
7866 }
7867 };
7868 },
7869
7870 // =paste
7871 paste: function () {
7872 return {
7873 init: function (e) {
7874 this.rtePaste = true;
7875 var pre = (this.opts.type === 'pre' || this.utils.isCurrentOrParent('pre')) ? true : false;
7876
7877 // clipboard event
7878 if (this.detect.isDesktop()) {
7879
7880 if (!this.paste.pre && this.opts.clipboardImageUpload && this.opts.imageUpload && this.paste.detectClipboardUpload(
7881 e)) {
7882 if (this.detect.isIe()) {
7883 setTimeout($.proxy(this.paste.clipboardUpload, this),
7884 100
7885 );
7886 }
7887
7888 return;
7889 }
7890 }
7891
7892 this.utils.saveScroll();
7893 this.selection.save();
7894 this.paste.createPasteBox(pre);
7895
7896 $(window).on('scroll.redactor-freeze', $.proxy(function () {
7897 $(window).scrollTop(this.saveBodyScroll);
7898
7899 }, this));
7900
7901 setTimeout($.proxy(function () {
7902 var html = this.paste.getPasteBoxCode(pre);
7903
7904 // buffer
7905 this.buffer.set();
7906 this.selection.restore();
7907
7908 this.utils.restoreScroll();
7909
7910 // paste info
7911 var data = this.clean.getCurrentType(html);
7912
7913 // clean
7914 html = this.clean.onPaste(html, data);
7915
7916 // callback
7917 var returned = this.core.callback('paste', html);
7918 html = (typeof returned === 'undefined') ? html : returned;
7919
7920 this.paste.insert(html, data);
7921 this.rtePaste = false;
7922
7923 // clean pre breaklines
7924 if (pre) {
7925 this.clean.cleanPre();
7926 }
7927
7928 $(window).off('scroll.redactor-freeze');
7929
7930 }, this), 1);
7931
7932 },
7933 getPasteBoxCode: function (pre) {
7934 var html = (pre) ? this.$pasteBox.val() : this.$pasteBox.html();
7935 this.$pasteBox.remove();
7936
7937 return html;
7938 },
7939 createPasteBox: function (pre) {
7940 var css = {
7941 position: 'fixed',
7942 width: '1px',
7943 top: 0,
7944 left: '-9999px'
7945 };
7946
7947 this.$pasteBox = (pre) ? $('<textarea>').css(css) : $('<div>').attr('contenteditable',
7948 'true'
7949 ).css(css);
7950 this.paste.appendPasteBox();
7951 this.$pasteBox.focus();
7952 },
7953 appendPasteBox: function () {
7954 if (this.detect.isIe()) {
7955 this.core.box().append(this.$pasteBox);
7956 }
7957 else {
7958 // bootstrap modal
7959 var $visibleModals = $('.modal-body:visible');
7960 if ($visibleModals.length > 0) {
7961 $visibleModals.append(this.$pasteBox);
7962 }
7963 else {
7964 $('body').prepend(this.$pasteBox);
7965 }
7966 }
7967 },
7968 detectClipboardUpload: function (e) {
7969 e = e.originalEvent || e;
7970
7971 var clipboard = e.clipboardData;
7972 if (this.detect.isIe() || this.detect.isFirefox()) {
7973 return false;
7974 }
7975
7976 // prevent safari fake url
7977 var types = clipboard.types;
7978 if (types.indexOf('public.tiff') !== -1) {
7979 e.preventDefault();
7980 return false;
7981 }
7982
7983 if (!clipboard.items || !clipboard.items.length) {
7984 return;
7985 }
7986
7987 var file = clipboard.items[0].getAsFile();
7988 if (file === null) {
7989 return false;
7990 }
7991
7992 var reader = new FileReader();
7993 reader.readAsDataURL(file);
7994 reader.onload = $.proxy(this.paste.insertFromClipboard, this);
7995
7996 return true;
7997 },
7998 clipboardUpload: function () {
7999 var imgs = this.$editor.find('img');
8000 $.each(imgs, $.proxy(function (i, s) {
8001 if (s.src.search(/^data\:image/i) === -1) {
8002 return;
8003 }
8004
8005 var formData = !!window.FormData ? new FormData() : null;
8006 if (!window.FormData) {
8007 return;
8008 }
8009
8010 this.upload.direct = true;
8011 this.upload.type = 'image';
8012 this.upload.url = this.opts.imageUpload;
8013 this.upload.callback = $.proxy(function (data) {
8014 if (this.detect.isIe()) {
8015 $(s).wrap($('<figure />'));
8016 }
8017
8018 else {
8019 var $parent = $(s).parent();
8020 this.utils.replaceToTag($parent, 'figure');
8021 }
8022
8023 s.src = data.url;
8024 this.core.callback('imageUpload', $(s), data);
8025
8026 }, this);
8027
8028 var blob = this.utils.dataURItoBlob(s.src);
8029
8030 formData.append('clipboard', 1);
8031 formData.append(this.opts.imageUploadParam, blob);
8032
8033 this.upload.send(formData, false);
8034 this.code.sync();
8035 this.rtePaste = false;
8036
8037 }, this));
8038 },
8039 insertFromClipboard: function (e) {
8040 var formData = !!window.FormData ? new FormData() : null;
8041 if (!window.FormData) {
8042 return;
8043 }
8044
8045 this.upload.direct = true;
8046 this.upload.type = 'image';
8047 this.upload.url = this.opts.imageUpload;
8048 this.upload.callback = this.image.insert;
8049
8050 var blob = this.utils.dataURItoBlob(e.target.result);
8051
8052 formData.append('clipboard', 1);
8053 formData.append(this.opts.imageUploadParam, blob);
8054
8055 this.upload.send(formData, e);
8056 this.rtePaste = false;
8057 },
8058 insert: function (html, data) {
8059 if (data.pre) {
8060 this.insert.raw(html);
8061 }
8062 else if (data.text) {
8063 this.insert.text(html);
8064 }
8065 else {
8066 this.insert.html(html, data);
8067 }
8068
8069 // Firefox Clipboard Observe
8070 if (this.detect.isFirefox() && this.opts.imageUpload && this.opts.clipboardImageUpload) {
8071 setTimeout($.proxy(this.paste.clipboardUpload, this), 100);
8072 }
8073
8074 }
8075 };
8076 },
8077
8078 // =placeholder -- UNSUPPORTED MODULE
8079 placeholder: function () {
8080 return {
8081 enable: function () {},
8082 show: function () {},
8083 update: function () {},
8084 hide: function () {},
8085 is: function () {},
8086 init: function () {},
8087 enabled: function () {},
8088 enableEvents: function () {},
8089 disableEvents: function () {},
8090 build: function () {},
8091 buildPosition: function () {},
8092 getPosition: function () {},
8093 isEditorEmpty: function () {},
8094 isAttr: function () {},
8095 destroy: function () {}
8096 };
8097 },
8098
8099 // =progress -- UNSUPPORTED MODULE
8100 progress: function () {
8101 return {
8102 $box: null,
8103 $bar: null,
8104 target: document.body, // or id selector
8105 show: function () {},
8106 hide: function () {},
8107 update: function () {},
8108 is: function () {},
8109 build: function () {},
8110 destroy: function () {}
8111 };
8112 },
8113
8114 // =selection
8115 selection: function () {
8116 return {
8117 get: function () {
8118 if (window.getSelection) {
8119 return window.getSelection();
8120 }
8121 else if (document.selection && document.selection.type !== 'Control') {
8122 return document.selection;
8123 }
8124
8125 return null;
8126 },
8127 range: function (sel) {
8128 if (typeof sel === 'undefined') {
8129 sel = this.selection.get();
8130 }
8131
8132 if (sel.getRangeAt && sel.rangeCount) {
8133 return sel.getRangeAt(0);
8134 }
8135
8136 return null;
8137 },
8138 is: function () {
8139 return (this.selection.isCollapsed()) ? false : true;
8140 },
8141 isRedactor: function () {
8142 var range = this.selection.range();
8143
8144 if (range !== null) {
8145 var el = range.startContainer.parentNode;
8146
8147 if ($(el).hasClass('redactor-in') || $(el).parents('.redactor-in').length !== 0) {
8148 return true;
8149 }
8150 }
8151
8152 return false;
8153 },
8154 isCollapsed: function () {
8155 var sel = this.selection.get();
8156
8157 return (sel === null) ? false : sel.isCollapsed;
8158 },
8159 update: function (sel, range) {
8160 if (range === null) {
8161 return;
8162 }
8163
8164 sel.removeAllRanges();
8165 sel.addRange(range);
8166 },
8167 current: function () {
8168 var sel = this.selection.get();
8169
8170 return (sel === null) ? false : sel.anchorNode;
8171 },
8172 parent: function () {
8173 var current = this.selection.current();
8174
8175 return (current === null) ? false : current.parentNode;
8176 },
8177 block: function (node) {
8178 node = node || this.selection.current();
8179
8180 while (node) {
8181 if (this.utils.isBlockTag(node.tagName)) {
8182 return ($(node).hasClass('redactor-in')) ? false : node;
8183 }
8184
8185 node = node.parentNode;
8186 }
8187
8188 return false;
8189 },
8190 inline: function (node) {
8191 node = node || this.selection.current();
8192
8193 while (node) {
8194 if (this.utils.isInlineTag(node.tagName)) {
8195 return ($(node).hasClass('redactor-in')) ? false : node;
8196 }
8197
8198 node = node.parentNode;
8199 }
8200
8201 return false;
8202 },
8203 element: function (node) {
8204 if (!node) {
8205 node = this.selection.current();
8206 }
8207
8208 while (node) {
8209 if (node.nodeType === 1) {
8210 if ($(node).hasClass('redactor-in')) {
8211 return false;
8212 }
8213
8214 return node;
8215 }
8216
8217 node = node.parentNode;
8218 }
8219
8220 return false;
8221 },
8222 prev: function () {
8223 var current = this.selection.current();
8224
8225 return (current === null) ? false : this.selection.current().previousSibling;
8226 },
8227 next: function () {
8228 var current = this.selection.current();
8229
8230 return (current === null) ? false : this.selection.current().nextSibling;
8231 },
8232 blocks: function (tag) {
8233 var blocks = [];
8234 var nodes = this.selection.nodes(tag);
8235
8236 $.each(nodes, $.proxy(function (i, node) {
8237 if (this.utils.isBlock(node)) {
8238 blocks.push(node);
8239 }
8240
8241 }, this));
8242
8243 var block = this.selection.block();
8244 if (blocks.length === 0 && block === false) {
8245 return [];
8246 }
8247 else if (blocks.length === 0 && block !== false) {
8248 return [block];
8249 }
8250 else {
8251 return blocks;
8252 }
8253
8254 },
8255 inlines: function (tag) {
8256 var inlines = [];
8257 var nodes = this.selection.nodes(tag);
8258
8259 $.each(nodes, $.proxy(function (i, node) {
8260 if (this.utils.isInline(node)) {
8261 inlines.push(node);
8262 }
8263
8264 }, this));
8265
8266 var inline = this.selection.inline();
8267 if (inlines.length === 0 && inline === false) {
8268 return [];
8269 }
8270 else if (inlines.length === 0 && inline !== false) {
8271 return [inline];
8272 }
8273 else {
8274 return inlines;
8275 }
8276 },
8277 nodes: function (tag) {
8278 var filter = (typeof tag === 'undefined') ? [] : (($.isArray(tag)) ? tag : [tag]);
8279
8280 var sel = this.selection.get();
8281 var range = this.selection.range(sel);
8282 var nodes = [];
8283 var resultNodes = [];
8284
8285 if (this.utils.isCollapsed()) {
8286 nodes = [this.selection.current()];
8287 }
8288 else {
8289 var node = range.startContainer;
8290 var endNode = range.endContainer;
8291
8292 // single node
8293 if (node === endNode) {
8294 return [node];
8295 }
8296
8297 // iterate
8298 while (node && node !== endNode) {
8299 nodes.push(node = this.selection.nextNode(node));
8300 }
8301
8302 // partially selected nodes
8303 node = range.startContainer;
8304 while (node && node !== range.commonAncestorContainer) {
8305 nodes.unshift(node);
8306 node = node.parentNode;
8307 }
8308 }
8309
8310 // remove service nodes
8311 $.each(nodes, function (i, s) {
8312 if (s) {
8313 var tagName = (s.nodeType !== 1) ? false : s.tagName.toLowerCase();
8314
8315 if ($(s).hasClass('redactor-script-tag') || $(s).hasClass(
8316 'redactor-selection-marker')) {
8317 return;
8318 }
8319 else if (tagName && filter.length !== 0 && $.inArray(tagName,
8320 filter
8321 ) === -1) {
8322 return;
8323 }
8324 else {
8325 resultNodes.push(s);
8326 }
8327 }
8328 });
8329
8330 return (resultNodes.length === 0) ? [] : resultNodes;
8331 },
8332 nextNode: function (node) {
8333 if (node.hasChildNodes()) {
8334 return node.firstChild;
8335 }
8336 else {
8337 while (node && !node.nextSibling) {
8338 node = node.parentNode;
8339 }
8340
8341 if (!node) {
8342 return null;
8343 }
8344
8345 return node.nextSibling;
8346 }
8347 },
8348 save: function () {
8349 this.marker.insert();
8350 this.savedSel = this.core.editor().html();
8351 },
8352 restore: function (removeMarkers) {
8353 var node1 = this.marker.find(1);
8354 var node2 = this.marker.find(2);
8355
8356 if (this.detect.isFirefox()) {
8357 this.core.editor().focus();
8358 }
8359
8360 if (node1.length !== 0 && node2.length !== 0) {
8361 this.caret.set(node1, node2);
8362 }
8363 else if (node1.length !== 0) {
8364 this.caret.start(node1);
8365 }
8366 else {
8367 this.core.editor().focus();
8368 }
8369
8370 if (removeMarkers !== false) {
8371 this.marker.remove();
8372 this.savedSel = false;
8373 }
8374 },
8375 saveInstant: function () {
8376 var el = this.core.editor()[0];
8377 var doc = el.ownerDocument, win = doc.defaultView;
8378 var sel = win.getSelection();
8379
8380 if (!sel.getRangeAt || !sel.rangeCount) {
8381 return;
8382 }
8383
8384 var range = sel.getRangeAt(0);
8385 var selectionRange = range.cloneRange();
8386
8387 selectionRange.selectNodeContents(el);
8388 selectionRange.setEnd(range.startContainer, range.startOffset);
8389
8390 var start = selectionRange.toString().length;
8391
8392 this.saved = {
8393 start: start,
8394 end: start + range.toString().length,
8395 node: range.startContainer
8396 };
8397
8398 return this.saved;
8399 },
8400 restoreInstant: function (saved) {
8401 if (typeof saved === 'undefined' && !this.saved) {
8402 return;
8403 }
8404
8405 this.saved = (typeof saved !== 'undefined') ? saved : this.saved;
8406
8407 var $node = this.core.editor().find(this.saved.node);
8408 if ($node.length !== 0 && $node.text().trim().replace(/\u200B/g,
8409 ''
8410 ).length === 0) {
8411 try {
8412 var range = document.createRange();
8413 range.setStart($node[0], 0);
8414
8415 var sel = window.getSelection();
8416 sel.removeAllRanges();
8417 sel.addRange(range);
8418 }
8419 catch (e) {}
8420
8421 return;
8422 }
8423
8424 var el = this.core.editor()[0];
8425 var doc = el.ownerDocument, win = doc.defaultView;
8426 var charIndex = 0, range = doc.createRange();
8427
8428 range.setStart(el, 0);
8429 range.collapse(true);
8430
8431 var nodeStack = [el], node, foundStart = false, stop = false;
8432 while (!stop && (node = nodeStack.pop())) {
8433 if (node.nodeType == 3) {
8434 var nextCharIndex = charIndex + node.length;
8435 if (!foundStart && this.saved.start >= charIndex && this.saved.start <= nextCharIndex) {
8436 range.setStart(node, this.saved.start - charIndex);
8437 foundStart = true;
8438 }
8439
8440 if (foundStart && this.saved.end >= charIndex && this.saved.end <= nextCharIndex) {
8441 range.setEnd(node, this.saved.end - charIndex);
8442 stop = true;
8443 }
8444 charIndex = nextCharIndex;
8445 }
8446 else {
8447 var i = node.childNodes.length;
8448 while (i--) {
8449 nodeStack.push(node.childNodes[i]);
8450 }
8451 }
8452 }
8453
8454 var sel = win.getSelection();
8455 sel.removeAllRanges();
8456 sel.addRange(range);
8457 },
8458 node: function (node) {
8459 $(node).prepend(this.marker.get(1));
8460 $(node).append(this.marker.get(2));
8461
8462 this.selection.restore();
8463 },
8464 all: function () {
8465 this.core.editor().focus();
8466
8467 var sel = this.selection.get();
8468 var range = this.selection.range(sel);
8469
8470 range.selectNodeContents(this.core.editor()[0]);
8471
8472 this.selection.update(sel, range);
8473 },
8474 remove: function () {
8475 this.selection.get().removeAllRanges();
8476 },
8477 replace: function (html) {
8478 this.insert.html(html);
8479 },
8480 text: function () {
8481 return this.selection.get().toString();
8482 },
8483 html: function () {
8484 var html = '';
8485 var sel = this.selection.get();
8486
8487 if (sel.rangeCount) {
8488 var container = document.createElement('div');
8489 var len = sel.rangeCount;
8490 for (var i = 0; i < len; ++i) {
8491 container.appendChild(sel.getRangeAt(i).cloneContents());
8492 }
8493
8494 html = this.clean.onGet(container.innerHTML);
8495 }
8496
8497 return html;
8498 },
8499 extractEndOfNode: function (node) {
8500 var sel = this.selection.get();
8501 var range = this.selection.range(sel);
8502
8503 var clonedRange = range.cloneRange();
8504 clonedRange.selectNodeContents(node);
8505 clonedRange.setStart(range.endContainer, range.endOffset);
8506
8507 return clonedRange.extractContents();
8508 },
8509
8510 // #backward
8511 removeMarkers: function () {
8512 this.marker.remove();
8513 },
8514 marker: function (num) {
8515 return this.marker.get(num);
8516 },
8517 markerHtml: function (num) {
8518 return this.marker.html(num);
8519 }
8520
8521 };
8522 },
8523
8524 // =shortcuts
8525 shortcuts: function () {
8526 return {
8527 // based on https://github.com/jeresig/jquery.hotkeys
8528 hotkeysSpecialKeys: {
8529 8: 'backspace',
8530 9: 'tab',
8531 10: 'return',
8532 13: 'return',
8533 16: 'shift',
8534 17: 'ctrl',
8535 18: 'alt',
8536 19: 'pause',
8537 20: 'capslock',
8538 27: 'esc',
8539 32: 'space',
8540 33: 'pageup',
8541 34: 'pagedown',
8542 35: 'end',
8543 36: 'home',
8544 37: 'left',
8545 38: 'up',
8546 39: 'right',
8547 40: 'down',
8548 45: 'insert',
8549 46: 'del',
8550 59: ';',
8551 61: '=',
8552 96: '0',
8553 97: '1',
8554 98: '2',
8555 99: '3',
8556 100: '4',
8557 101: '5',
8558 102: '6',
8559 103: '7',
8560 104: '8',
8561 105: '9',
8562 106: '*',
8563 107: '+',
8564 109: '-',
8565 110: '.',
8566 111: '/',
8567 112: 'f1',
8568 113: 'f2',
8569 114: 'f3',
8570 115: 'f4',
8571 116: 'f5',
8572 117: 'f6',
8573 118: 'f7',
8574 119: 'f8',
8575 120: 'f9',
8576 121: 'f10',
8577 122: 'f11',
8578 123: 'f12',
8579 144: 'numlock',
8580 145: 'scroll',
8581 173: '-',
8582 186: ';',
8583 187: '=',
8584 188: ',',
8585 189: '-',
8586 190: '.',
8587 191: '/',
8588 192: '`',
8589 219: '[',
8590 220: '\\',
8591 221: ']',
8592 222: '\''
8593 },
8594 hotkeysShiftNums: {
8595 '`': '~',
8596 '1': '!',
8597 '2': '@',
8598 '3': '#',
8599 '4': '$',
8600 '5': '%',
8601 '6': '^',
8602 '7': '&',
8603 '8': '*',
8604 '9': '(',
8605 '0': ')',
8606 '-': '_',
8607 '=': '+',
8608 ';': ': ',
8609 '\'': '"',
8610 ',': '<',
8611 '.': '>',
8612 '/': '?',
8613 '\\': '|'
8614 },
8615 init: function (e, key) {
8616 // disable browser's hot keys for bold and italic if shortcuts off
8617 if (this.opts.shortcuts === false) {
8618 if ((e.ctrlKey || e.metaKey) && (key === 66 || key === 73)) {
8619 e.preventDefault();
8620 }
8621
8622 return false;
8623 }
8624 else {
8625 // build
8626 $.each(this.opts.shortcuts, $.proxy(function (str, command) {
8627 this.shortcuts.build(e, str, command);
8628
8629 }, this));
8630 }
8631 },
8632 build: function (e, str, command) {
8633 var handler = $.proxy(function () {
8634 this.shortcuts.buildHandler(command);
8635
8636 }, this);
8637
8638 var keys = str.split(',');
8639 var len = keys.length;
8640 for (var i = 0; i < len; i++) {
8641 if (typeof keys[i] === 'string') {
8642 this.shortcuts.handler(e, $.trim(keys[i]), handler);
8643 }
8644 }
8645
8646 },
8647 buildHandler: function (command) {
8648 var func;
8649 if (command.func.search(/\./) !== '-1') {
8650 func = command.func.split('.');
8651 if (typeof this[func[0]] !== 'undefined') {
8652 this[func[0]][func[1]].apply(this, command.params);
8653 }
8654 }
8655 else {
8656 this[command.func].apply(this, command.params);
8657 }
8658 },
8659 handler: function (e, keys, origHandler) {
8660 keys = keys.toLowerCase().split(' ');
8661
8662 var special = this.shortcuts.hotkeysSpecialKeys[e.keyCode];
8663 var character = String.fromCharCode(e.which).toLowerCase();
8664 var modif = '', possible = {};
8665
8666 $.each(['alt', 'ctrl', 'meta', 'shift'], function (index, specialKey) {
8667 if (e[specialKey + 'Key'] && special !== specialKey) {
8668 modif += specialKey + '+';
8669 }
8670 });
8671
8672 if (special) {
8673 possible[modif + special] = true;
8674 }
8675
8676 if (character) {
8677 possible[modif + character] = true;
8678 possible[modif + this.shortcuts.hotkeysShiftNums[character]] = true;
8679
8680 // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
8681 if (modif === 'shift+') {
8682 possible[this.shortcuts.hotkeysShiftNums[character]] = true;
8683 }
8684 }
8685
8686 var len = keys.length;
8687 for (var i = 0; i < len; i++) {
8688 if (possible[keys[i]]) {
8689 e.preventDefault();
8690 return origHandler.apply(this, arguments);
8691 }
8692 }
8693 }
8694 };
8695 },
8696
8697 // =storage -- UNSUPPORTED MODULE
8698 storage: function () {
8699 return {
8700 data: [],
8701 add: function () {},
8702 status: function () {},
8703 observe: function () {},
8704 changes: function () {}
8705
8706 };
8707 },
8708
8709 // =toolbar
8710 toolbar: function () {
8711 return {
8712 build: function () {
8713 this.button.hideButtons();
8714 this.button.hideButtonsOnMobile();
8715
8716 this.$toolbarBox = $('<div class="redactor-toolbar-box" />');
8717 this.$toolbarBox[0].innerHTML = '<ul class="redactor-toolbar" id="redactor-toolbar-' + this.uuid + '" role="toolbar"></ul>';
8718 this.$toolbar = $(this.$toolbarBox[0].children[0]);
8719 this.$box[0].insertBefore(this.$toolbarBox[0], this.$box[0].firstChild);
8720
8721 this.button.$toolbar = this.$toolbar;
8722 this.button.setFormatting();
8723 this.button.load(this.$toolbar);
8724
8725 require(['Core'], (function(Core) {
8726 this.$toolbar[0].addEventListener('keydown', this.toolbar.keydown.bind(this, Core));
8727 }).bind(this));
8728 },
8729 createContainer: function () {},
8730 append: function () {},
8731 setOverflow: function () {},
8732 setFixed: function () {},
8733 setUnfixed: function () {},
8734 getBoxTop: function () {},
8735 observeScroll: function () {},
8736 observeScrollResize: function () {},
8737 observeScrollEnable: function () {},
8738 observeScrollDisable: function () {},
8739 setDropdownsFixed: function () {},
8740 unsetDropdownsFixed: function () {},
8741 setDropdownPosition: function () {},
8742 /**
8743 * @param {object} Core
8744 * @param {KeyboardEvent} event
8745 */
8746 keydown: function(Core, event) {
8747 var activeButton = document.activeElement;
8748 if (!activeButton.classList.contains('re-button')) {
8749 return;
8750 }
8751
8752 // Enter, Space, End, Home, ArrowLeft, ArrowRight, ArrowDown
8753 // Remarks: ArrowUp is not considered, because we do not support radio groups at the top level.
8754 var keyboardCodes = [13, 32, 35, 36, 37, 39, 40];
8755 if (keyboardCodes.indexOf(event.which) === -1) {
8756 return;
8757 }
8758
8759 // [Enter] || [Space]
8760 if (event.which === 13 || event.which === 32) {
8761 event.preventDefault();
8762
8763 require(['Core'], function(Core) {
8764 Core.triggerEvent(activeButton, 'mousedown');
8765 });
8766
8767 return;
8768 }
8769
8770 // [ArrowDown] opens drop-down menus, but does nothing on "regular" buttons.
8771 if (event.which === 40) {
8772 if (elAttr(activeButton, 'aria-haspopup') !== 'true') {
8773 return;
8774 }
8775
8776 event.preventDefault();
8777 Core.triggerEvent(activeButton, 'mousedown');
8778
8779 var dropdown = $(activeButton).data('dropdown');
8780 var firstItem = elBySel('li', dropdown[0]);
8781 if (firstItem) firstItem.focus();
8782 return;
8783 }
8784
8785 event.preventDefault();
8786
8787 var buttons = Array.prototype.slice.call(elBySelAll('.re-button', this.$toolbar[0]));
8788 var newActiveButton = null;
8789 // [End]
8790 if (event.which === 35) {
8791 newActiveButton = buttons[buttons.length - 1];
8792 }
8793 // [Home]
8794 else if (event.which === 36) {
8795 newActiveButton = buttons[0];
8796 }
8797 else {
8798 var index = buttons.indexOf(activeButton);
8799
8800 // [ArrowLeft]
8801 if (event.which === 37) {
8802 index--;
8803
8804 if (index === -1) {
8805 index = buttons.length - 1;
8806 }
8807 }
8808 // [ArrowRight]
8809 else if (event.which === 39) {
8810 index++;
8811
8812 if (index === buttons.length) {
8813 index = 0;
8814 }
8815 }
8816
8817 newActiveButton = buttons[index];
8818 }
8819
8820 if (newActiveButton !== null) {
8821 newActiveButton.focus();
8822 }
8823 }
8824 };
8825 },
8826
8827 // =upload -- UNSUPPORTED MODULE
8828 upload: function () {
8829 return {
8830 init: function () {},
8831 directUpload: function () {},
8832 onDrop: function () {},
8833 traverseFile: function () {},
8834 setConfig: function () {},
8835 getType: function () {},
8836 getHiddenFields: function () {},
8837 send: function () {},
8838 onDrag: function () {},
8839 onDragLeave: function () {},
8840 clearImageFields: function () {},
8841 addImageFields: function () {},
8842 removeImageFields: function () {},
8843 clearFileFields: function () {},
8844 addFileFields: function () {},
8845 removeFileFields: function () {}
8846 };
8847 },
8848
8849 // =s3 -- UNSUPPORTED MODULE
8850 uploads3: function () {
8851 return {
8852 send: function () {},
8853 executeOnSignedUrl: function () {},
8854 createCORSRequest: function () {},
8855 sendToS3: function () {}
8856 };
8857 },
8858
8859 // =utils
8860 utils: function () {
8861 return {
8862 isEmpty: function (html) {
8863 html = (typeof html === 'undefined') ? this.core.editor().html() : html;
8864
8865 html = html.replace(/[\u200B-\u200D\uFEFF]/g, '');
8866 html = html.replace(/&nbsp;/gi, '');
8867 html = html.replace(/<\/?br\s?\/?>/g, '');
8868 html = html.replace(/\s/g, '');
8869 html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
8870 html = html.replace(/<iframe(.*?[^>])>$/i, 'iframe');
8871 html = html.replace(/<source(.*?[^>])>$/i, 'source');
8872
8873 // remove empty tags
8874 html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8875 html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8876
8877 html = $.trim(html);
8878
8879 return html === '';
8880 },
8881 isElement: function (obj) {
8882 try {
8883 // Using W3 DOM2 (works for FF, Opera and Chrome)
8884 return obj instanceof HTMLElement;
8885 }
8886 catch (e) {
8887 return (typeof obj === 'object') && (obj.nodeType === 1) && (typeof obj.style === 'object') && (typeof obj.ownerDocument === 'object');
8888 }
8889 },
8890 strpos: function (haystack, needle, offset) {
8891 var i = haystack.indexOf(needle, offset);
8892 return i >= 0 ? i : false;
8893 },
8894 dataURItoBlob: function (dataURI) {
8895 var byteString;
8896 if (dataURI.split(',')[0].indexOf('base64') >= 0) {
8897 byteString = atob(dataURI.split(',')[1]);
8898 }
8899 else {
8900 byteString = unescape(dataURI.split(',')[1]);
8901 }
8902
8903 var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
8904
8905 var ia = new Uint8Array(byteString.length);
8906 for (var i = 0; i < byteString.length; i++) {
8907 ia[i] = byteString.charCodeAt(i);
8908 }
8909
8910 return new Blob([ia], {type: mimeString});
8911 },
8912 getOuterHtml: function (el) {
8913 return $('<div>').append($(el).eq(0).clone()).html();
8914 },
8915 cloneAttributes: function (from, to) {
8916 from = from[0] || from;
8917 to = $(to);
8918
8919 var attrs = from.attributes;
8920 var len = attrs.length;
8921 while (len--) {
8922 var attr = attrs[len];
8923 to.attr(attr.name, attr.value);
8924 }
8925
8926 return to;
8927 },
8928 breakBlockTag: function () {
8929 var block = this.selection.block();
8930 if (!block) {
8931 return false;
8932 }
8933
8934 var isEmpty = this.utils.isEmpty(block.innerHTML);
8935
8936 var tag = block.tagName.toLowerCase();
8937 if (tag === 'pre' || tag === 'li' || tag === 'td' || tag === 'th') {
8938 return false;
8939 }
8940
8941 if (!isEmpty && this.utils.isStartOfElement(block)) {
8942 return {
8943 $block: $(block),
8944 $next: $(block).next(),
8945 type: 'start'
8946 };
8947 }
8948 else if (!isEmpty && this.utils.isEndOfElement(block)) {
8949 return {
8950 $block: $(block),
8951 $next: $(block).next(),
8952 type: 'end'
8953 };
8954 }
8955 else {
8956 var endOfNode = this.selection.extractEndOfNode(block);
8957 var $nextPart = $('<' + tag + ' />').append(endOfNode);
8958
8959 $nextPart = this.utils.cloneAttributes(block, $nextPart);
8960 $(block).after($nextPart);
8961
8962 return {
8963 $block: $(block),
8964 $next: $nextPart,
8965 type: 'break'
8966 };
8967 }
8968 }, // tag detection
8969 inBlocks: function (tags) {
8970 tags = ($.isArray(tags)) ? tags : [tags];
8971
8972 var blocks = this.selection.blocks();
8973 var len = blocks.length;
8974 var contains = false;
8975 for (var i = 0; i < len; i++) {
8976 if (blocks[i] !== false) {
8977 var tag = blocks[i].tagName.toLowerCase();
8978
8979 if ($.inArray(tag, tags) !== -1) {
8980 contains = true;
8981 }
8982 }
8983 }
8984
8985 return contains;
8986
8987 },
8988 inInlines: function (tags) {
8989 tags = ($.isArray(tags)) ? tags : [tags];
8990
8991 var inlines = this.selection.inlines();
8992 var len = inlines.length;
8993 var contains = false;
8994 for (var i = 0; i < len; i++) {
8995 var tag = inlines[i].tagName.toLowerCase();
8996
8997 if ($.inArray(tag, tags) !== -1) {
8998 contains = true;
8999 }
9000 }
9001
9002 return contains;
9003
9004 },
9005 isTag: function (current, tag) {
9006 var element = $(current).closest(tag, this.core.editor()[0]);
9007 if (element.length === 1) {
9008 return element[0];
9009 }
9010
9011 return false;
9012 },
9013 isBlock: function (block) {
9014 if (block === null) {
9015 return false;
9016 }
9017
9018 block = block[0] || block;
9019
9020 return block && this.utils.isBlockTag(block.tagName);
9021 },
9022 isBlockTag: function (tag) {
9023 return (typeof tag === 'undefined') ? false : this.reIsBlock.test(tag);
9024 },
9025 isInline: function (inline) {
9026 inline = inline[0] || inline;
9027
9028 return inline && this.utils.isInlineTag(inline.tagName);
9029 },
9030 isInlineTag: function (tag) {
9031 return (typeof tag === 'undefined') ? false : this.reIsInline.test(tag);
9032 }, // parents detection
9033 isRedactorParent: function (el) {
9034 if (!el) {
9035 return false;
9036 }
9037
9038 if ($(el).parents('.redactor-in').length === 0 || $(el).hasClass('redactor-in')) {
9039 return false;
9040 }
9041
9042 return el;
9043 },
9044 isCurrentOrParentHeader: function () {
9045 return this.utils.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
9046 },
9047 isCurrentOrParent: function (tagName) {
9048 var parent = this.selection.parent();
9049 var current = this.selection.current();
9050
9051 if ($.isArray(tagName)) {
9052 var matched = 0;
9053 $.each(tagName, $.proxy(function (i, s) {
9054 if (this.utils.isCurrentOrParentOne(current, parent, s)) {
9055 matched++;
9056 }
9057 }, this));
9058
9059 return (matched === 0) ? false : true;
9060 }
9061 else {
9062 return this.utils.isCurrentOrParentOne(current, parent, tagName);
9063 }
9064 },
9065 isCurrentOrParentOne: function (current, parent, tagName) {
9066 tagName = tagName.toUpperCase();
9067
9068 return parent && parent.tagName === tagName ? parent : current && current.tagName === tagName ? current : false;
9069 },
9070 isEditorRelative: function () {
9071 var position = this.core.editor().css('position');
9072 var arr = ['absolute', 'fixed', 'relative'];
9073
9074 return ($.inArray(arr, position) !== -1);
9075 },
9076 setEditorRelative: function () {
9077 this.core.editor().addClass('redactor-relative');
9078 }, // scroll
9079 getScrollTarget: function () {
9080 var $scrollTarget = $(this.opts.scrollTarget);
9081
9082 return ($scrollTarget.length !== 0) ? $scrollTarget : $(document);
9083 },
9084 freezeScroll: function () {
9085 this.freezeScrollTop = this.utils.getScrollTarget().scrollTop();
9086 this.utils.getScrollTarget().scrollTop(this.freezeScrollTop);
9087 },
9088 unfreezeScroll: function () {
9089 if (typeof this.freezeScrollTop === 'undefined') {
9090 return;
9091 }
9092
9093 this.utils.getScrollTarget().scrollTop(this.freezeScrollTop);
9094 },
9095 saveScroll: function () {
9096 this.tmpScrollTop = this.utils.getScrollTarget().scrollTop();
9097 },
9098 restoreScroll: function () {
9099 if (typeof this.tmpScrollTop === 'undefined') {
9100 return;
9101 }
9102
9103 this.utils.getScrollTarget().scrollTop(this.tmpScrollTop);
9104 },
9105 isStartOfElement: function (element) {
9106 if (typeof element === 'undefined') {
9107 element = this.selection.block();
9108 if (!element) {
9109 return false;
9110 }
9111 }
9112
9113 return (this.offset.get(element) === 0) ? true : false;
9114 },
9115 isEndOfElement: function (element) {
9116 if (typeof element === 'undefined') {
9117 element = this.selection.block();
9118 if (!element) {
9119 return false;
9120 }
9121 }
9122
9123 var text = $.trim($(element).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
9124 ''
9125 );
9126 var offset = this.offset.get(element);
9127
9128 return (offset === text.length) ? true : false;
9129 },
9130 removeEmptyAttr: function (el, attr) {
9131 var $el = $(el);
9132 if (typeof $el.attr(attr) === 'undefined') {
9133 return true;
9134 }
9135
9136 if ($el.attr(attr) === '') {
9137 $el.removeAttr(attr);
9138 return true;
9139 }
9140
9141 return false;
9142 },
9143 replaceToTag: function (node, tag) {
9144 var replacement;
9145 $(node).replaceWith(function () {
9146 replacement = $('<' + tag + ' />').append($(this).contents());
9147
9148 for (var i = 0; i < this.attributes.length; i++) {
9149 replacement.attr(this.attributes[i].name,
9150 this.attributes[i].value
9151 );
9152 }
9153
9154 return replacement;
9155 });
9156
9157 return replacement;
9158 }, // select all
9159 isSelectAll: function () {
9160 return this.selectAll;
9161 },
9162 enableSelectAll: function () {
9163 this.selectAll = true;
9164 },
9165 disableSelectAll: function () {
9166 this.selectAll = false;
9167 },
9168 disableBodyScroll: function () {},
9169 measureScrollbar: function () {
9170 var $body = $('body');
9171 var scrollDiv = document.createElement('div');
9172 scrollDiv.className = 'redactor-scrollbar-measure';
9173
9174 $body.append(scrollDiv);
9175 var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
9176 $body[0].removeChild(scrollDiv);
9177 return scrollbarWidth;
9178 },
9179 enableBodyScroll: function () {},
9180 appendFields: function (appendFields, data) {
9181 if (!appendFields) {
9182 return data;
9183 }
9184 else if (typeof appendFields === 'object') {
9185 $.each(appendFields, function (k, v) {
9186 if (v !== null && v.toString().indexOf('#') === 0) {
9187 v = $(v).val();
9188 }
9189
9190 data.append(k, v);
9191
9192 });
9193
9194 return data;
9195 }
9196
9197 var $fields = $(appendFields);
9198 if ($fields.length === 0) {
9199 return data;
9200 }
9201 else {
9202 var str = '';
9203 $fields.each(function () {
9204 data.append($(this).attr('name'), $(this).val());
9205 });
9206
9207 return data;
9208 }
9209 },
9210 appendForms: function (appendForms, data) {
9211 if (!appendForms) {
9212 return data;
9213 }
9214
9215 var $forms = $(appendForms);
9216 if ($forms.length === 0) {
9217 return data;
9218 }
9219 else {
9220 var formData = $forms.serializeArray();
9221
9222 $.each(formData, function (z, f) {
9223 data.append(f.name, f.value);
9224 });
9225
9226 return data;
9227 }
9228 },
9229 isRgb: function (str) {
9230 return (str.search(/^rgb/i) === 0);
9231 },
9232 rgb2hex: function (rgb) {
9233 rgb = rgb.match(
9234 /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
9235
9236 return (rgb && rgb.length === 4) ? '#' + ('0' + parseInt(rgb[1],
9237 10
9238 ).toString(16)).slice(-2) + ('0' + parseInt(rgb[2],
9239 10
9240 ).toString(16)).slice(-2) + ('0' + parseInt(rgb[3],
9241 10
9242 ).toString(16)).slice(-2) : '';
9243 },
9244
9245 // #backward
9246 isCollapsed: function () {
9247 return this.selection.isCollapsed();
9248 },
9249 isMobile: function () {
9250 return this.detect.isMobile();
9251 },
9252 isDesktop: function () {
9253 return this.detect.isDesktop();
9254 },
9255 isPad: function () {
9256 return this.detect.isIpad();
9257 }
9258
9259 };
9260 },
9261
9262 // #backward
9263 browser: function () {
9264 return {
9265 webkit: function () {
9266 return this.detect.isWebkit();
9267 },
9268 ff: function () {
9269 return this.detect.isFirefox();
9270 },
9271 ie: function () {
9272 return this.detect.isIe();
9273 }
9274 };
9275 }
9276 };
9277
9278 $(window).on('load.tools.redactor', function () {
9279 $('[data-tools="redactor"]').redactor();
9280 });
9281
9282 // constructor
9283 Redactor.prototype.init.prototype = Redactor.prototype;
9284
9285 })(jQuery);
9286
9287 (function ($) {
9288 $.fn.redactorAnimation = function (animation, options, callback) {
9289 return this.each(function () {
9290 new redactorAnimation(this, animation, options, callback);
9291 });
9292 };
9293
9294 function redactorAnimation(element, animation, options, callback) {
9295 // default
9296 var opts = {
9297 duration: 0.5,
9298 iterate: 1,
9299 delay: 0,
9300 prefix: 'redactor-',
9301 timing: 'linear'
9302 };
9303
9304 this.animation = animation;
9305 this.slide = (this.animation === 'slideDown' || this.animation === 'slideUp');
9306 this.$element = $(element);
9307 this.prefixes = ['', '-moz-', '-o-animation-', '-webkit-'];
9308 this.queue = [];
9309
9310 // options or callback
9311 if (typeof options === 'function') {
9312 callback = options;
9313 this.opts = opts;
9314 }
9315 else {
9316 this.opts = $.extend(opts, options);
9317 }
9318
9319 // slide
9320 if (this.slide) {
9321 this.$element.height(this.$element.height());
9322 }
9323
9324 // init
9325 this.init(callback);
9326
9327 }
9328
9329 redactorAnimation.prototype = {
9330
9331 init: function (callback) {
9332 this.queue.push(this.animation);
9333
9334 this.clean();
9335
9336 if (this.animation === 'show') {
9337 this.opts.timing = 'linear';
9338 this.$element.removeClass('hide').show();
9339
9340 if (typeof callback === 'function') {
9341 callback(this);
9342 }
9343 }
9344 else if (this.animation === 'hide') {
9345 this.opts.timing = 'linear';
9346 this.$element.hide();
9347
9348 if (typeof callback === 'function') {
9349 callback(this);
9350 }
9351 }
9352 else {
9353 this.animate(callback);
9354 }
9355
9356 },
9357 animate: function (callback) {
9358 this.$element.addClass('redactor-animated').css('display', '').removeClass('hide');
9359 this.$element.addClass(this.opts.prefix + this.queue[0]);
9360
9361 this.set(this.opts.duration + 's', this.opts.delay + 's', this.opts.iterate, this.opts.timing);
9362
9363 var _callback = (this.queue.length > 1) ? null : callback;
9364 this.complete('AnimationEnd', $.proxy(function () {
9365 if (this.$element.hasClass(this.opts.prefix + this.queue[0])) {
9366 this.clean();
9367 this.queue.shift();
9368
9369 if (this.queue.length) {
9370 this.animate(callback);
9371 }
9372 }
9373
9374 }, this), _callback);
9375 },
9376 set: function (duration, delay, iterate, timing) {
9377 var len = this.prefixes.length;
9378
9379 while (len--) {
9380 this.$element.css(this.prefixes[len] + 'animation-duration', duration);
9381 this.$element.css(this.prefixes[len] + 'animation-delay', delay);
9382 this.$element.css(this.prefixes[len] + 'animation-iteration-count', iterate);
9383 this.$element.css(this.prefixes[len] + 'animation-timing-function', timing);
9384 }
9385
9386 },
9387 clean: function () {
9388 this.$element.removeClass('redactor-animated');
9389 this.$element.removeClass(this.opts.prefix + this.queue[0]);
9390
9391 this.set('', '', '', '');
9392
9393 },
9394 complete: function (type, make, callback) {
9395 this.$element.one(type.toLowerCase() + ' webkit' + type + ' o' + type + ' MS' + type,
9396 $.proxy(function () {
9397 if (typeof make === 'function') {
9398 make();
9399 }
9400
9401 if (typeof callback === 'function') {
9402 callback(this);
9403 }
9404
9405 // hide
9406 var effects = [
9407 'fadeOut',
9408 'slideUp',
9409 'zoomOut',
9410 'slideOutUp',
9411 'slideOutRight',
9412 'slideOutLeft'
9413 ];
9414 if ($.inArray(this.animation, effects) !== -1) {
9415 this.$element.css('display', 'none');
9416 }
9417
9418 // slide
9419 if (this.slide) {
9420 this.$element.css('height', '');
9421 }
9422
9423 }, this)
9424 );
9425
9426 }
9427 };
9428 })(jQuery);