Merge branch '5.2' into 5.3
[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 this.selection.save();
4731
4732 var nodes = this.inline.getClearedNodes();
4733 this.inline.setNodesStriked(nodes, tag, params);
4734
4735 this.selection.restore();
4736
4737 document.execCommand('strikethrough');
4738
4739 this.selection.saveInstant();
4740
4741 // WoltLab: Chrome misbehaves in some cases, causing the `<strike>` element for
4742 // contained elements to be stripped. Instead, those children are assigned the
4743 // CSS style `text-decoration-line: line-through`.
4744 var chromeElements = this.core.editor()[0].querySelectorAll('[style*="line-through"]'), element, strike;
4745 for (var i = 0, length = chromeElements.length; i < length; i++) {
4746 element = chromeElements[0];
4747
4748 strike = document.createElement('strike');
4749 element.parentNode.insertBefore(strike, element);
4750 strike.appendChild(element);
4751
4752 // Remove the bogus style attribute.
4753 element.style.removeProperty('text-decoration');
4754 }
4755
4756 var self = this;
4757 this.core.editor().find('strike').each(function () {
4758 var $el = self.utils.replaceToTag(this, tag);
4759 self.inline.setParams($el[0], params);
4760
4761 var $inside = $el.find(tag);
4762 var $parent = $el.parent();
4763 var $parentAround = $parent.parent();
4764
4765 // revert formatting (safari bug)
4766 if ($parentAround.length !== 0 && $parentAround[0].tagName.toLowerCase() === tag && $parentAround.html() == $parent[0].outerHTML) {
4767 $el.replaceWith(function () { return $(this).contents(); });
4768 $parentAround.replaceWith(function () { return $(this).contents(); });
4769
4770 return;
4771 }
4772
4773 // remove inside
4774 if ($inside.length !== 0) {
4775 self.inline.cleanInsideOrParent($inside, params);
4776 }
4777
4778 // same parent
4779 if ($parent.html() == $el[0].outerHTML) {
4780 self.inline.cleanInsideOrParent($parent, params);
4781 }
4782
4783 // bugfix: remove empty inline tags after selection
4784 if (self.detect.isFirefox()) {
4785 self.core.editor().find(tag + ':empty').remove();
4786 }
4787 });
4788
4789 this.selection.restoreInstant();
4790 },
4791 cleanInsideOrParent: function ($el, params) {
4792 if (params) {
4793 for (var key in params.data) {
4794 this.inline.removeSpecificAttr($el, key, params.data[key]);
4795 }
4796 }
4797 },
4798 getClearedNodes: function () {
4799 var nodes = this.selection.nodes();
4800 var newNodes = [];
4801 var len = nodes.length;
4802 var started = 0;
4803
4804 // find array slice
4805 for (var i = 0; i < len; i++) {
4806 if ($(nodes[i]).hasClass('redactor-selection-marker')) {
4807 started = i + 2;
4808 break;
4809 }
4810 }
4811
4812 // find selected inline & text nodes
4813 for (var i = 0; i < len; i++) {
4814 if (i >= started && !this.utils.isBlockTag(nodes[i].tagName)) {
4815 newNodes.push(nodes[i]);
4816 }
4817 }
4818
4819 return newNodes;
4820 },
4821 isConvertableAttr: function (node, name, value) {
4822 var nodeAttrValue = $(node).attr(name);
4823 if (nodeAttrValue) {
4824 if (name === 'style') {
4825 value = $.trim(value).replace(/;$/, '');
4826
4827 var rules = value.split(';');
4828 var count = 0;
4829 for (var i = 0; i < rules.length; i++) {
4830 var arr = rules[i].split(':');
4831 var ruleName = $.trim(arr[0]);
4832 var ruleValue = $.trim(arr[1]);
4833
4834 if (ruleName.search(/color/) !== -1) {
4835 var val = $(node).css(ruleName);
4836 if (val && (val === ruleValue || this.utils.rgb2hex(
4837 val) === ruleValue)) {
4838 count++;
4839 }
4840 }
4841 else if ($(node).css(ruleName) === ruleValue) {
4842 count++;
4843 }
4844 }
4845
4846 if (count === rules.length) {
4847 return 1;
4848 }
4849 }
4850 else if (nodeAttrValue === value) {
4851 return 1;
4852 }
4853 }
4854
4855 return 0;
4856
4857 },
4858 isConvertable: function (node, nodeTag, tag, params) {
4859 if (nodeTag === tag) {
4860 if (params) {
4861 var count = 0;
4862 for (var key in params.data) {
4863 count += this.inline.isConvertableAttr(node,
4864 key,
4865 params.data[key]
4866 );
4867 }
4868
4869 if (count === Object.keys(params.data).length) {
4870 return true;
4871 }
4872 }
4873 else {
4874 return true;
4875 }
4876 }
4877
4878 return false;
4879 },
4880 setNodesStriked: function (nodes, tag, params) {
4881 for (var i = 0; i < nodes.length; i++) {
4882 var nodeTag = (nodes[i].tagName) ? nodes[i].tagName.toLowerCase() : undefined;
4883
4884 var parent = nodes[i].parentNode;
4885 var parentTag = (parent && parent.tagName) ? parent.tagName.toLowerCase() : undefined;
4886
4887 var convertable = this.inline.isConvertable(parent,
4888 parentTag,
4889 tag,
4890 params
4891 );
4892 if (convertable) {
4893 var $el = $(parent).replaceWith(function () {
4894 return $('<strike>').append($(this).contents());
4895 });
4896
4897 $el.attr('data-redactor-inline-converted');
4898 }
4899
4900 var convertable = this.inline.isConvertable(nodes[i],
4901 nodeTag,
4902 tag,
4903 params
4904 );
4905 if (convertable) {
4906 var $el = $(nodes[i]).replaceWith(function () {
4907 return $('<strike>').append($(this).contents());
4908 });
4909 }
4910 }
4911 },
4912 insertBreakpoint: function (inline, currentTag) {
4913 var breakpoint = document.createElement('span');
4914 breakpoint.id = 'redactor-inline-breakpoint';
4915 breakpoint = this.insert.node(breakpoint);
4916
4917 var end = this.utils.isEndOfElement(inline);
4918 var code = this.utils.getOuterHtml(inline);
4919 var endTag = (end) ? '' : '<' + currentTag + '>';
4920
4921 code = code.replace(/<span id="redactor-inline-breakpoint"><\/span>/i,
4922 '</' + currentTag + '>' + endTag
4923 );
4924
4925 var $code = $(code);
4926 $(inline).replaceWith($code);
4927
4928 if (endTag !== '') {
4929 this.utils.cloneAttributes(inline, $code.last());
4930 }
4931
4932 return $code.first();
4933 },
4934 insertInline: function (tag) {
4935 var node = document.createElement(tag);
4936
4937 this.insert.node(node);
4938 this.caret.start(node);
4939
4940 return node;
4941 },
4942 arrangeTag: function (tag) {
4943 var tags = [
4944 'b',
4945 'bold',
4946 'i',
4947 'italic',
4948 'underline',
4949 'strikethrough',
4950 'deleted',
4951 'superscript',
4952 'subscript'
4953 ];
4954 var replaced = [
4955 'strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub'
4956 ];
4957
4958 tag = tag.toLowerCase();
4959
4960 for (var i = 0; i < tags.length; i++) {
4961 if (tag === tags[i]) {
4962 tag = replaced[i];
4963 }
4964 }
4965
4966 return tag;
4967 },
4968 getStyleParams: function (params) {
4969 var result = {};
4970 var rules = params.trim().replace(/;$/, '').split(';');
4971 for (var i = 0; i < rules.length; i++) {
4972 var rule = rules[i].split(':');
4973 if (rule) {
4974 result[rule[0].trim()] = rule[1].trim();
4975 }
4976 }
4977
4978 return result;
4979 },
4980 getParams: function (attr, value, type) {
4981 var data = false;
4982 var func = 'toggle';
4983 if (typeof attr === 'object') {
4984 data = attr;
4985 func = (value !== undefined) ? value : func;
4986 }
4987 else if (attr !== undefined && value !== undefined) {
4988 data = {};
4989 data[attr] = value;
4990 func = (type !== undefined) ? type : func;
4991 }
4992
4993 return (data) ? {
4994 'func': func,
4995 'data': data
4996 } : false;
4997 },
4998 setParams: function (node, params) {
4999 if (params) {
5000 for (var key in params.data) {
5001 var $node = $(node);
5002 if (key === 'style') {
5003 node = this.inline[params.func + 'Style'](params.data[key],
5004 node
5005 );
5006 $node.attr('data-redactor-style-cache',
5007 $node.attr('style')
5008 );
5009 }
5010 else if (key === 'class') {
5011 node = this.inline[params.func + 'Class'](params.data[key],
5012 node
5013 );
5014 }
5015 // attr
5016 else {
5017 node = (params.func === 'remove') ? this.inline[params.func + 'Attr'](key,
5018 node
5019 ) : this.inline[params.func + 'Attr'](key,
5020 params.data[key],
5021 node
5022 );
5023 }
5024
5025 if (key === 'style' && node.tagName === 'SPAN') {
5026 $node.attr('data-redactor-span', true);
5027 }
5028 }
5029 }
5030
5031 return node;
5032 },
5033
5034 // Each
5035 eachInline: function (node, callback) {
5036 var lastNode;
5037 var nodes = (node === undefined) ? this.selection.inlines() : [node];
5038 if (nodes) {
5039 for (var i = 0; i < nodes.length; i++) {
5040 lastNode = callback(nodes[i])[0];
5041 }
5042 }
5043
5044 return lastNode;
5045 },
5046
5047 // Class
5048 replaceClass: function (value, node) {
5049 return this.inline.eachInline(node, function (el) {
5050 return $(el).removeAttr('class').addClass(value);
5051 });
5052 },
5053 toggleClass: function (value, node) {
5054 return this.inline.eachInline(node, function (el) {
5055 return $(el).toggleClass(value);
5056 });
5057 },
5058 addClass: function (value, node) {
5059 return this.inline.eachInline(node, function (el) {
5060 return $(el).addClass(value);
5061 });
5062 },
5063 removeClass: function (value, node) {
5064 return this.inline.eachInline(node, function (el) {
5065 return $(el).removeClass(value);
5066 });
5067 },
5068 removeAllClass: function (node) {
5069 return this.inline.eachInline(node, function (el) {
5070 return $(el).removeAttr('class');
5071 });
5072 },
5073
5074 // Attr
5075 replaceAttr: function (name, value, node) {
5076 return this.inline.eachInline(node, function (el) {
5077 return $(el).removeAttr(name).attr(name.value);
5078 });
5079 },
5080 toggleAttr: function (name, value, node) {
5081 return this.inline.eachInline(node, function (el) {
5082 var attr = $(el).attr(name);
5083
5084 return (attr) ? $(el).removeAttr(name) : $(el).attr(name.value);
5085 });
5086 },
5087 addAttr: function (name, value, node) {
5088 return this.inline.eachInline(node, function (el) {
5089 return $(el).attr(name, value);
5090 });
5091 },
5092 removeAttr: function (name, node) {
5093 return this.inline.eachInline(node, function (el) {
5094 var $el = $(el);
5095
5096 $el.removeAttr(name);
5097 if (name === 'style') {
5098 $el.removeAttr('data-redactor-style-cache');
5099 }
5100
5101 return $el;
5102 });
5103 },
5104 removeAllAttr: function (node) {
5105 return this.inline.eachInline(node, function (el) {
5106 var $el = $(el);
5107 var len = el.attributes.length;
5108 for (var z = 0; z < len; z++) {
5109 $el.removeAttr(el.attributes[z].name);
5110 }
5111
5112 return $el;
5113 });
5114 },
5115 removeSpecificAttr: function (node, key, value) {
5116 var $el = $(node);
5117 if (key === 'style') {
5118 var arr = value.split(':');
5119 var name = arr[0].trim();
5120 $el.css(name, '');
5121
5122 if (this.utils.removeEmptyAttr(node, 'style')) {
5123 $el.removeAttr('data-redactor-style-cache');
5124 }
5125 }
5126 else {
5127 $el.removeAttr(key)[0];
5128 }
5129 },
5130
5131 // Style
5132 hasParentStyle: function ($el) {
5133 var $parent = $el.parent();
5134
5135 return ($parent.length === 1 && $parent[0].tagName === $el[0].tagName && $parent.html() === $el[0].outerHTML) ? $parent : false;
5136 },
5137 addParentStyle: function ($el) {
5138 var $parent = this.inline.hasParentStyle($el);
5139 if ($parent) {
5140 var style = this.inline.getStyleParams($el.attr('style'));
5141 $parent.css(style);
5142 $parent.attr('data-redactor-style-cache', $parent.attr('style'));
5143
5144 $el.replaceWith(function () {
5145 return $(this).contents();
5146 });
5147 }
5148 else {
5149 $el.attr('data-redactor-style-cache', $el.attr('style'));
5150 }
5151
5152 return $el;
5153 },
5154 replaceStyle: function (params, node) {
5155 params = this.inline.getStyleParams(params);
5156
5157 var self = this;
5158 return this.inline.eachInline(node, function (el) {
5159 var $el = $(el);
5160 $el.removeAttr('style').css(params);
5161
5162 var style = $el.attr('style');
5163 if (style) $el.attr('style', style.replace(/"/g, '\''));
5164
5165 $el = self.inline.addParentStyle($el);
5166
5167 return $el;
5168 });
5169 },
5170 toggleStyle: function (params, node) {
5171 params = this.inline.getStyleParams(params);
5172
5173 var self = this;
5174 return this.inline.eachInline(node, function (el) {
5175 var $el = $(el);
5176
5177 for (var key in params) {
5178 var newVal = params[key];
5179 var oldVal = $el.css(key);
5180
5181 oldVal = (self.utils.isRgb(oldVal)) ? self.utils.rgb2hex(oldVal) : oldVal.replace(/"/g,
5182 ''
5183 );
5184 newVal = (self.utils.isRgb(newVal)) ? self.utils.rgb2hex(newVal) : newVal.replace(/"/g,
5185 ''
5186 );
5187
5188 if (oldVal === newVal) {
5189 $el.css(key, '');
5190 }
5191 else {
5192 $el.css(key, newVal);
5193 }
5194 }
5195
5196 var style = $el.attr('style');
5197 if (style) $el.attr('style', style.replace(/"/g, '\''));
5198
5199 if (!self.utils.removeEmptyAttr(el, 'style')) {
5200 $el = self.inline.addParentStyle($el);
5201 }
5202 else {
5203 $el.removeAttr('data-redactor-style-cache');
5204 }
5205
5206 return $el;
5207 });
5208 },
5209 addStyle: function (params, node) {
5210 params = this.inline.getStyleParams(params);
5211
5212 var self = this;
5213 return this.inline.eachInline(node, function (el) {
5214 var $el = $(el);
5215
5216 $el.css(params);
5217
5218 var style = $el.attr('style');
5219 if (style) $el.attr('style', style.replace(/"/g, '\''));
5220
5221 $el = self.inline.addParentStyle($el);
5222
5223 return $el;
5224 });
5225 },
5226 removeStyle: function (params, node) {
5227 params = this.inline.getStyleParams(params);
5228
5229 var self = this;
5230 return this.inline.eachInline(node, function (el) {
5231 var $el = $(el);
5232
5233 for (var key in params) {
5234 $el.css(key, '');
5235 }
5236
5237 if (self.utils.removeEmptyAttr(el, 'style')) {
5238 $el.removeAttr('data-redactor-style-cache');
5239 }
5240 else {
5241 $el.attr('data-redactor-style-cache', $el.attr('style'));
5242 }
5243
5244 return $el;
5245 });
5246 },
5247 removeAllStyle: function (node) {
5248 return this.inline.eachInline(node, function (el) {
5249 return $(el).removeAttr('style').removeAttr('data-redactor-style-cache');
5250 });
5251 },
5252 removeStyleRule: function (name) {
5253 var parent = this.selection.parent();
5254 var nodes = this.selection.inlines();
5255
5256 this.buffer.set();
5257
5258 if (parent && parent.tagName === 'SPAN') {
5259 this.inline.removeStyleRuleAttr($(parent), name);
5260 }
5261
5262 for (var i = 0; i < nodes.length; i++) {
5263 var el = nodes[i];
5264 var $el = $(el);
5265 if ($.inArray(el.tagName.toLowerCase(),
5266 this.opts.inlineTags
5267 ) != -1 && !$el.hasClass('redactor-selection-marker')) {
5268 this.inline.removeStyleRuleAttr($el, name);
5269 }
5270 }
5271
5272 },
5273 removeStyleRuleAttr: function ($el, name) {
5274 $el.css(name, '');
5275 if (this.utils.removeEmptyAttr($el, 'style')) {
5276 $el.removeAttr('data-redactor-style-cache');
5277 }
5278 else {
5279 $el.attr('data-redactor-style-cache', $el.attr('style'));
5280 }
5281 },
5282
5283 // Update
5284 update: function (tag, attr, value, type) {
5285 tag = this.inline.arrangeTag(tag);
5286
5287 var params = this.inline.getParams(attr, value, type);
5288 var nodes = this.selection.inlines();
5289 var result = [];
5290
5291 if (nodes) {
5292 for (var i = 0; i < nodes.length; i++) {
5293 var el = nodes[i];
5294 if (tag === '*' || el.tagName.toLowerCase() === tag) {
5295 result.push(this.inline.setParams(el, params));
5296 }
5297 }
5298 }
5299
5300 return result;
5301 },
5302
5303 // All
5304 removeFormat: function () {
5305 this.selection.save();
5306
5307 var nodes = this.inline.getClearedNodes();
5308 for (var i = 0; i < nodes.length; i++) {
5309 if (nodes[i].nodeType === 1) {
5310 $(nodes[i]).replaceWith(function () {
5311 return $(this).contents();
5312 });
5313 }
5314 }
5315
5316 this.selection.restore();
5317 }
5318
5319 };
5320 },
5321
5322 // =insert
5323 insert: function () {
5324 return {
5325 set: function (html) {
5326 this.code.set(html);
5327 this.focus.end();
5328 },
5329 html: function (html, data) {
5330 this.core.editor().focus();
5331
5332 var block = this.selection.block();
5333 var inline = this.selection.inline();
5334
5335 // clean
5336 if (typeof data === 'undefined') {
5337 data = this.clean.getCurrentType(html, true);
5338 html = this.clean.onPaste(html, data, true);
5339 }
5340
5341 html = $.parseHTML(html);
5342
5343 // end node
5344 var endNode = $(html).last();
5345
5346 // delete selected content
5347 var sel = this.selection.get();
5348 var range = this.selection.range(sel);
5349 range.deleteContents();
5350
5351 this.selection.update(sel, range);
5352
5353 // insert list in list
5354 if (data.lists) {
5355 var $list = $(html);
5356 if ($list.length !== 0 && ($list[0].tagName === 'UL' || $list[0].tagName === 'OL')) {
5357
5358 this.insert.appendLists(block, $list);
5359 return;
5360 }
5361 }
5362
5363 if (data.blocks && block) {
5364 if (this.utils.isSelectAll()) {
5365 this.core.editor().html(html);
5366 this.focus.end();
5367 }
5368 else {
5369 var breaked = this.utils.breakBlockTag();
5370 if (breaked === false) {
5371 this.insert.placeHtml(html);
5372 }
5373 else {
5374 var $last = $(html).children().last();
5375 $last.append(this.marker.get());
5376
5377 if (breaked.type === 'start') {
5378 breaked.$block.before(html);
5379 }
5380 else {
5381 breaked.$block.after(html);
5382 }
5383
5384 this.selection.restore();
5385 this.core.editor().find('p').each(function () {
5386 if ($.trim(this.innerHTML) === '') {
5387 $(this).remove();
5388 }
5389 });
5390 }
5391 }
5392 }
5393 else {
5394 if (inline) {
5395 // remove same tag inside
5396 var $div = $('<div/>').html(html);
5397 $div.find(inline.tagName.toLowerCase()).each(function () {
5398 $(this).contents().unwrap();
5399 });
5400
5401 html = $div.html();
5402 html = $.parseHTML(html);
5403
5404 endNode = $(html).last();
5405
5406 }
5407
5408 if (this.utils.isSelectAll()) {
5409 var $node = $(this.opts.emptyHtml);
5410 this.core.editor().html('').append($node);
5411 $node.html(html);
5412 this.caret.end($node);
5413 }
5414 else {
5415 this.insert.placeHtml(html);
5416 }
5417 }
5418
5419 this.utils.disableSelectAll();
5420
5421 if (data.pre) this.clean.cleanPre();
5422
5423 this.caret.end(endNode);
5424 },
5425 text: function (text) {
5426 text = text.toString();
5427 text = $.trim(text);
5428
5429 var tmp = document.createElement('div');
5430 tmp.innerHTML = text;
5431 text = tmp.textContent || tmp.innerText;
5432
5433 if (typeof text === 'undefined') {
5434 return;
5435 }
5436
5437 this.core.editor().focus();
5438
5439 // blocks
5440 var blocks = this.selection.blocks();
5441
5442 // nl to spaces
5443 text = text.replace(/\n/g, ' ');
5444
5445 // select all
5446 if (this.utils.isSelectAll()) {
5447 var $node = $(this.opts.emptyHtml);
5448 this.core.editor().html('').append($node);
5449 $node.html(text);
5450 this.caret.end($node);
5451 }
5452 else {
5453 // insert
5454 var sel = this.selection.get();
5455 var node = document.createTextNode(text);
5456
5457 if (sel.getRangeAt && sel.rangeCount) {
5458 var range = sel.getRangeAt(0);
5459 range.deleteContents();
5460 range.insertNode(node);
5461 range.setStartAfter(node);
5462 range.collapse(true);
5463
5464 this.selection.update(sel, range);
5465 }
5466
5467 // wrap node if selected two or more block tags
5468 if (blocks.length > 1) {
5469 $(node).wrap('<p>');
5470 this.caret.after(node);
5471 }
5472 }
5473
5474 this.utils.disableSelectAll();
5475 this.clean.normalizeCurrentHeading();
5476
5477 },
5478 raw: function (html) {
5479 this.core.editor().focus();
5480
5481 var sel = this.selection.get();
5482
5483 var range = this.selection.range(sel);
5484 range.deleteContents();
5485
5486 var el = document.createElement('div');
5487 el.innerHTML = html;
5488
5489 var frag = document.createDocumentFragment(), node, lastNode;
5490 while ((node = el.firstChild)) {
5491 lastNode = frag.appendChild(node);
5492 }
5493
5494 range.insertNode(frag);
5495
5496 if (lastNode) {
5497 range = range.cloneRange();
5498 range.setStartAfter(lastNode);
5499 range.collapse(true);
5500 sel.removeAllRanges();
5501 sel.addRange(range);
5502 }
5503 },
5504 node: function (node, deleteContent) {
5505 if (typeof this.start !== 'undefined') {
5506 this.core.editor().focus();
5507 }
5508
5509 node = node[0] || node;
5510
5511 var block = this.selection.block();
5512 var gap = this.utils.isBlockTag(node.tagName);
5513 var result = true;
5514
5515 if (this.utils.isSelectAll()) {
5516 if (gap) {
5517 this.core.editor().html(node);
5518 }
5519 else {
5520 this.core.editor().html($('<p>').html(node));
5521 }
5522
5523 this.code.sync();
5524 }
5525 else if (gap && block) {
5526 var breaked = this.utils.breakBlockTag();
5527 if (breaked === false) {
5528 this.insert.placeNode(node, deleteContent);
5529 }
5530 else {
5531 if (breaked.type === 'start') {
5532 breaked.$block.before(node);
5533 }
5534 else {
5535 breaked.$block.after(node);
5536 }
5537
5538 this.core.editor().find('p:empty').remove();
5539 }
5540 }
5541 else {
5542 result = this.insert.placeNode(node, deleteContent);
5543 }
5544
5545 this.utils.disableSelectAll();
5546
5547 if (result) {
5548 this.caret.end(node);
5549 }
5550
5551 return node;
5552
5553 },
5554 appendLists: function (block, $list) {
5555 var $block = $(block);
5556 var last;
5557 var isEmpty = this.utils.isEmpty(block.innerHTML);
5558
5559 if (isEmpty || this.utils.isEndOfElement(block)) {
5560 last = $block;
5561 $list.find('li').each(function () {
5562 last.after(this);
5563 last = $(this);
5564 });
5565
5566 if (isEmpty) {
5567 $block.remove();
5568 }
5569 }
5570 else if (this.utils.isStartOfElement(block)) {
5571 $list.find('li').each(function () {
5572 $block.before(this);
5573 last = $(this);
5574 });
5575 }
5576 else {
5577 var endOfNode = this.selection.extractEndOfNode(block);
5578
5579 $block.after($('<li>').append(endOfNode));
5580 $block.append($list);
5581 last = $list;
5582 }
5583
5584 this.marker.remove();
5585
5586 if (last) {
5587 this.caret.end(last);
5588 }
5589 },
5590 placeHtml: function (html) {
5591 var marker = document.createElement('span');
5592 marker.id = 'redactor-insert-marker';
5593 marker = this.insert.node(marker);
5594
5595 $(marker).before(html);
5596 this.selection.restore();
5597 this.caret.after(marker);
5598 $(marker).remove();
5599 },
5600 placeNode: function (node, deleteContent) {
5601 var sel = this.selection.get();
5602 var range = this.selection.range(sel);
5603 if (range == null) {
5604 return false;
5605 }
5606
5607 if (deleteContent !== false) {
5608 range.deleteContents();
5609 }
5610
5611 range.insertNode(node);
5612 range.collapse(false);
5613
5614 this.selection.update(sel, range);
5615 },
5616 nodeToPoint: function (e, node) {
5617 node = node[0] || node;
5618
5619 if (this.utils.isEmpty()) {
5620 node = (this.utils.isBlock(node)) ? node : $('<p />').append(node);
5621
5622 this.core.editor().html(node);
5623
5624 return node;
5625 }
5626
5627 var range;
5628 var x = e.clientX, y = e.clientY;
5629 if (document.caretPositionFromPoint) {
5630 var pos = document.caretPositionFromPoint(x, y);
5631 var sel = document.getSelection();
5632 range = sel.getRangeAt(0);
5633 range.setStart(pos.offsetNode, pos.offset);
5634 range.collapse(true);
5635 range.insertNode(node);
5636 }
5637 else if (document.caretRangeFromPoint) {
5638 range = document.caretRangeFromPoint(x, y);
5639 range.insertNode(node);
5640 }
5641 else if (typeof document.body.createTextRange !== 'undefined') {
5642 range = document.body.createTextRange();
5643 range.moveToPoint(x, y);
5644 var endRange = range.duplicate();
5645 endRange.moveToPoint(x, y);
5646 range.setEndPoint('EndToEnd', endRange);
5647 range.select();
5648 }
5649
5650 return node;
5651
5652 },
5653
5654 // #backward
5655 nodeToCaretPositionFromPoint: function (e, node) {
5656 this.insert.nodeToPoint(e, node);
5657 },
5658 marker: function () {
5659 this.marker.insert();
5660 }
5661 };
5662 },
5663
5664 // =keydown
5665 keydown: function () {
5666 return {
5667 init: function (e) {
5668 if (this.rtePaste) {
5669 return;
5670 }
5671
5672 var key = e.which;
5673 var arrow = (key >= 37 && key <= 40);
5674
5675 this.keydown.ctrl = e.ctrlKey || e.metaKey;
5676 this.keydown.parent = this.selection.parent();
5677 this.keydown.current = this.selection.current();
5678 this.keydown.block = this.selection.block();
5679
5680 // detect tags
5681 this.keydown.pre = this.utils.isTag(this.keydown.current, 'pre');
5682 this.keydown.blockquote = this.utils.isTag(this.keydown.current, 'blockquote');
5683 this.keydown.figcaption = this.utils.isTag(this.keydown.current, 'figcaption');
5684 this.keydown.figure = this.utils.isTag(this.keydown.current, 'figure');
5685
5686 // callback
5687 var keydownStop = this.core.callback('keydown', e);
5688 if (keydownStop === false) {
5689 e.preventDefault();
5690 return false;
5691 }
5692
5693 // shortcuts setup
5694 this.shortcuts.init(e, key);
5695
5696 // buffer
5697 this.keydown.checkEvents(arrow, key);
5698 this.keydown.setupBuffer(e, key);
5699
5700 if (this.utils.isSelectAll() && (key === this.keyCode.ENTER || key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE)) {
5701 e.preventDefault();
5702
5703 this.code.set(this.opts.emptyHtml);
5704 this.events.changeHandler();
5705 return;
5706 }
5707
5708 this.keydown.addArrowsEvent(arrow);
5709 this.keydown.setupSelectAll(e, key);
5710
5711 // turn off enter key
5712 if (!this.opts.enterKey && key === this.keyCode.ENTER) {
5713 e.preventDefault();
5714
5715 // remove selected
5716 var sel = this.selection.get();
5717 var range = this.selection.range(sel);
5718
5719 if (!range.collapsed) {
5720 range.deleteContents();
5721 }
5722
5723 return;
5724 }
5725
5726 // down
5727 if (this.opts.enterKey && key === this.keyCode.DOWN) {
5728 this.keydown.onArrowDown();
5729 }
5730
5731 // up
5732 if (this.opts.enterKey && key === this.keyCode.UP) {
5733 this.keydown.onArrowUp();
5734 }
5735
5736 // replace to p before / after the table or into body
5737 if ((this.opts.type === 'textarea' || this.opts.type === 'div') && this.keydown.current && this.keydown.current.nodeType === 3 && $(
5738 this.keydown.parent).hasClass('redactor-in')) {
5739 this.keydown.wrapToParagraph();
5740 }
5741
5742 // on Shift+Space or Ctrl+Space
5743 if (!this.keyup.lastShiftKey && key === this.keyCode.SPACE && (e.ctrlKey || e.shiftKey)) {
5744 e.preventDefault();
5745
5746 return this.keydown.onShiftSpace();
5747 }
5748
5749 // on Shift+Enter or Ctrl+Enter
5750 if (key === this.keyCode.ENTER && (e.ctrlKey || e.shiftKey)) {
5751 // iOS Safari will report the shift key to be pressed, if the caret is at the
5752 // front of the line and the next character should be an uppercase character.
5753 if (Environment === null || Environment.platform() !== 'ios') {
5754 e.preventDefault();
5755
5756 return this.keydown.onShiftEnter(e);
5757 }
5758 }
5759
5760 // on enter
5761 if (key === this.keyCode.ENTER && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
5762 return this.keydown.onEnter(e);
5763 }
5764
5765 // tab or cmd + [
5766 if (key === this.keyCode.TAB || e.metaKey && key === 221 || e.metaKey && key === 219) {
5767 return this.keydown.onTab(e, key);
5768 }
5769
5770 // firefox bugfix
5771 if (this.detect.isFirefox() && key === this.keyCode.BACKSPACE && this.keydown.block && this.keydown.block.tagName === 'P' && this.utils.isStartOfElement(
5772 this.keydown.block)) {
5773 var $prev = $(this.keydown.block).prev();
5774 if ($prev.length !== 0) {
5775 e.preventDefault();
5776
5777 $prev.append(this.marker.get());
5778 $prev.append($(this.keydown.block).html());
5779 $(this.keydown.block).remove();
5780
5781 this.selection.restore();
5782
5783 return;
5784 }
5785 }
5786
5787 // backspace & delete
5788 if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) {
5789 if (this.observe.image && typeof this.observe.image !== 'undefined' && $(
5790 '#redactor-image-box').length !== 0) {
5791 e.preventDefault();
5792
5793 var $prev = this.observe.image.closest('figure, p').prev();
5794 this.image.remove(false);
5795 this.observe.image = false;
5796
5797 if ($prev && $prev.length !== 0) {
5798 this.caret.end($prev);
5799 }
5800 else {
5801 this.core.editor().focus();
5802 }
5803
5804 return;
5805 }
5806
5807 this.keydown.onBackspaceAndDeleteBefore();
5808 }
5809
5810 if (key === this.keyCode.DELETE) {
5811 var $next = $(this.keydown.block).next();
5812
5813 // delete figure
5814 if (this.utils.isEndOfElement(this.keydown.block) && $next.length !== 0 && $next[0].tagName === 'FIGURE') {
5815 $next.remove();
5816 return false;
5817 }
5818
5819 // append list (safari bug)
5820 var tagLi = (this.keydown.block && this.keydown.block.tagName === 'LI') ? this.keydown.block : false;
5821 if (tagLi) {
5822 var $list = $(this.keydown.block).parents('ul, ol').last();
5823 var $nextList = $list.next();
5824
5825 if (this.utils.isRedactorParent($list) && this.utils.isEndOfElement(
5826 $list) && $nextList.length !== 0 && ($nextList[0].tagName === 'UL' || $nextList[0].tagName === 'OL')) {
5827 e.preventDefault();
5828
5829 $list.append($nextList.contents());
5830 $nextList.remove();
5831
5832 return false;
5833 }
5834 }
5835
5836 // append pre
5837 if (this.utils.isEndOfElement(this.keydown.block) && $next.length !== 0 && $next[0].tagName === 'PRE') {
5838 $(this.keydown.block).append($next.text());
5839 $next.remove();
5840 return false;
5841 }
5842
5843 }
5844
5845 // image delete
5846 if (key === this.keyCode.DELETE && $('#redactor-image-box').length !== 0) {
5847 this.image.remove();
5848 }
5849
5850 // backspace
5851 if (key === this.keyCode.BACKSPACE) {
5852 if (this.detect.isFirefox()) {
5853 this.line.removeOnBackspace(e);
5854 }
5855
5856 // combine list after and before if paragraph is empty
5857 if (this.list.combineAfterAndBefore(this.keydown.block)) {
5858 e.preventDefault();
5859 return;
5860 }
5861
5862 // backspace as outdent
5863 var block = this.selection.block();
5864 if (block && block.tagName === 'LI' && this.utils.isCollapsed() && this.utils.isStartOfElement()) {
5865 this.indent.decrease();
5866 e.preventDefault();
5867 return;
5868 }
5869
5870 this.keydown.removeInvisibleSpace();
5871 this.keydown.removeEmptyListInTable(e);
5872
5873 }
5874
5875 if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) {
5876 this.keydown.onBackspaceAndDeleteAfter(e);
5877 }
5878
5879 },
5880 onShiftSpace: function () {
5881 this.buffer.set();
5882 this.insert.raw('&nbsp;');
5883
5884 return false;
5885 },
5886 onShiftEnter: function (e) {
5887 this.buffer.set();
5888
5889 return (this.keydown.pre) ? this.keydown.insertNewLine(e) : this.insert.raw(
5890 '<br>');
5891 },
5892 onBackspaceAndDeleteBefore: function () {
5893 this.utils.saveScroll();
5894 },
5895 onBackspaceAndDeleteAfter: function (e) {
5896 // remove style tag
5897 setTimeout($.proxy(function () {
5898 this.code.syncFire = false;
5899 this.keydown.removeEmptyLists();
5900
5901 var filter = '';
5902 if (this.opts.keepStyleAttr.length !== 0) {
5903 filter = ',' + this.opts.keepStyleAttr.join(',');
5904 }
5905
5906 var $styleTags = this.core.editor().find('*[style]');
5907 $styleTags.not(
5908 'img, figure, iframe, #redactor-image-box, #redactor-image-editter, [data-redactor-style-cache], [data-redactor-span]' + filter).removeAttr(
5909 'style');
5910
5911 this.keydown.formatEmpty(e);
5912 this.code.syncFire = true;
5913
5914 }, this), 1);
5915 },
5916 onEnter: function (e) {
5917 var stop = this.core.callback('enter', e);
5918 if (stop === false) {
5919 e.preventDefault();
5920 return false;
5921 }
5922
5923 // blockquote exit
5924 if (this.keydown.blockquote && this.keydown.exitFromBlockquote(e) === true) {
5925 return false;
5926 }
5927
5928 // pre
5929 if (this.keydown.pre) {
5930 return this.keydown.insertNewLine(e);
5931 }
5932 // blockquote & figcaption
5933 else if (this.keydown.blockquote || this.keydown.figcaption) {
5934 return this.keydown.insertBreakLine(e);
5935 }
5936 // figure
5937 else if (this.keydown.figure) {
5938 setTimeout($.proxy(function () {
5939 this.keydown.replaceToParagraph('FIGURE');
5940
5941 }, this), 1);
5942 }
5943 // paragraphs
5944 else if (this.keydown.block) {
5945 setTimeout($.proxy(function () {
5946 this.keydown.replaceToParagraph('DIV');
5947
5948 }, this), 1);
5949
5950 // empty list exit
5951 if (this.keydown.block.tagName === 'LI') {
5952 var current = this.selection.current();
5953 var $parent = $(current).closest('li', this.$editor[0]);
5954 var $list = $parent.parents('ul,ol', this.$editor[0]).last();
5955
5956 if ($parent.length !== 0 && this.utils.isEmpty($parent.html()) && $list.next().length === 0 && this.utils.isEmpty(
5957 $list.find('li').last().html())) {
5958 $list.find('li').last().remove();
5959
5960 var node = $(this.opts.emptyHtml);
5961 $list.after(node);
5962 this.caret.start(node);
5963
5964 return false;
5965 }
5966 }
5967
5968 }
5969 // outside
5970 else if (!this.keydown.block) {
5971 return this.keydown.insertParagraph(e);
5972 }
5973
5974 // firefox enter into inline element
5975 if (this.detect.isFirefox() && this.utils.isInline(this.keydown.parent)) {
5976 this.keydown.insertBreakLine(e);
5977 return;
5978 }
5979
5980 // remove inline tags in new-empty paragraph
5981 if (!this.opts.keepInlineOnEnter) {
5982 setTimeout($.proxy(function () {
5983 var inline = this.selection.inline();
5984 if (inline && this.utils.isEmpty(inline.innerHTML)) {
5985 var parent = this.selection.block();
5986 $(inline).remove();
5987 //this.caret.start(parent);
5988
5989 var range = document.createRange();
5990 range.setStart(parent, 0);
5991
5992 var textNode = document.createTextNode('\u200B');
5993
5994 range.insertNode(textNode);
5995 range.setStartAfter(textNode);
5996 range.collapse(true);
5997
5998 var sel = window.getSelection();
5999 sel.removeAllRanges();
6000 sel.addRange(range);
6001 }
6002
6003 }, this), 1);
6004 }
6005 },
6006 checkEvents: function (arrow, key) {
6007 if (!arrow && (this.core.getEvent() === 'click' || this.core.getEvent() === 'arrow')) {
6008 this.core.addEvent(false);
6009
6010 if (this.keydown.checkKeyEvents(key)) {
6011 this.buffer.set();
6012 }
6013 }
6014 },
6015 checkKeyEvents: function (key) {
6016 var k = this.keyCode;
6017 var keys = [
6018 k.BACKSPACE,
6019 k.DELETE,
6020 k.ENTER,
6021 k.ESC,
6022 k.TAB,
6023 k.CTRL,
6024 k.META,
6025 k.ALT,
6026 k.SHIFT
6027 ];
6028
6029 return ($.inArray(key, keys) === -1) ? true : false;
6030
6031 },
6032 addArrowsEvent: function (arrow) {
6033 if (!arrow) {
6034 return;
6035 }
6036
6037 if ((this.core.getEvent() === 'click' || this.core.getEvent() === 'arrow')) {
6038 this.core.addEvent(false);
6039 return;
6040 }
6041
6042 this.core.addEvent('arrow');
6043 },
6044 setupBuffer: function (e, key) {
6045 if (this.keydown.ctrl && key === 90 && !e.shiftKey && !e.altKey && this.sBuffer.length) // z key
6046 {
6047 e.preventDefault();
6048 this.buffer.undo();
6049 return;
6050 }
6051 // redo
6052 else if (this.keydown.ctrl && key === 90 && e.shiftKey && !e.altKey && this.sRebuffer.length !== 0) {
6053 e.preventDefault();
6054 this.buffer.redo();
6055 return;
6056 }
6057 else if (!this.keydown.ctrl) {
6058 if (key === this.keyCode.SPACE || key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE || (key === this.keyCode.ENTER && !e.ctrlKey && !e.shiftKey)) {
6059 this.buffer.set();
6060 }
6061 }
6062 },
6063 exitFromBlockquote: function (e) {
6064 if (!this.utils.isEndOfElement(this.keydown.blockquote)) {
6065 return;
6066 }
6067
6068 var tmp = this.clean.removeSpacesHard($(this.keydown.blockquote).html());
6069 if (tmp.search(/(<br\s?\/?>){1}$/i) !== -1) {
6070 e.preventDefault();
6071
6072 var $last = $(this.keydown.blockquote).children().last();
6073
6074 $last.filter('br').remove();
6075 $(this.keydown.blockquote).children().last().filter('span').remove();
6076
6077 var node = $(this.opts.emptyHtml);
6078 $(this.keydown.blockquote).after(node);
6079 this.caret.start(node);
6080
6081 return true;
6082
6083 }
6084
6085 return;
6086 },
6087 onArrowDown: function () {
6088 var tags = [this.keydown.blockquote, this.keydown.pre, this.keydown.figcaption];
6089
6090 for (var i = 0; i < tags.length; i++) {
6091 if (tags[i]) {
6092 this.keydown.insertAfterLastElement(tags[i]);
6093 return false;
6094 }
6095 }
6096 },
6097 onArrowUp: function () {
6098 var tags = [this.keydown.blockquote, this.keydown.pre, this.keydown.figcaption];
6099
6100 for (var i = 0; i < tags.length; i++) {
6101 if (tags[i]) {
6102 this.keydown.insertBeforeFirstElement(tags[i]);
6103 return false;
6104 }
6105 }
6106 },
6107 insertAfterLastElement: function (element) {
6108 if (!this.utils.isEndOfElement(element)) {
6109 return;
6110 }
6111
6112 var last = this.core.editor().contents().last();
6113 var $next = (element.tagName === 'FIGCAPTION') ? $(this.keydown.block).parent().next() : $(
6114 this.keydown.block).next();
6115
6116 if ($next.length !== 0) {
6117 return;
6118 }
6119 else if (last.length === 0 && last[0] !== element) {
6120 this.caret.start(last);
6121 return;
6122 }
6123 else {
6124 var node = $(this.opts.emptyHtml);
6125
6126 if (element.tagName === 'FIGCAPTION') {
6127 $(element).parent().after(node);
6128 }
6129 else {
6130 $(element).after(node);
6131 }
6132
6133 this.caret.start(node);
6134 }
6135
6136 },
6137 insertBeforeFirstElement: function (element) {
6138 if (!this.utils.isStartOfElement()) {
6139 return;
6140 }
6141
6142 if (this.core.editor().contents().length > 1 && this.core.editor().contents().first()[0] !== element) {
6143 return;
6144 }
6145
6146 var node = $(this.opts.emptyHtml);
6147 $(element).before(node);
6148 this.caret.start(node);
6149
6150 },
6151 onTab: function (e, key) {
6152 if (!this.opts.tabKey) {
6153 return true;
6154 }
6155
6156 var isList = (this.keydown.block && this.keydown.block.tagName === 'LI');
6157 if (this.utils.isEmpty(this.code.get()) || (!isList && !this.keydown.pre && this.opts.tabAsSpaces === false)) {
6158 return true;
6159 }
6160
6161 e.preventDefault();
6162 this.buffer.set();
6163
6164 var isListStart = (isList && this.utils.isStartOfElement(this.keydown.block));
6165 var node;
6166
6167 if (this.keydown.pre && !e.shiftKey) {
6168 node = (this.opts.preSpaces) ? document.createTextNode(Array(this.opts.preSpaces + 1).join(
6169 '\u00a0')) : document.createTextNode('\t');
6170 this.insert.node(node);
6171 }
6172 else if (this.opts.tabAsSpaces !== false && !isListStart) {
6173 node = document.createTextNode(Array(this.opts.tabAsSpaces + 1).join(
6174 '\u00a0'));
6175 this.insert.node(node);
6176 }
6177 else {
6178 if (e.metaKey && key === 219) {
6179 this.indent.decrease();
6180 }
6181 else if (e.metaKey && key === 221) {
6182 this.indent.increase();
6183 }
6184 else if (!e.shiftKey) {
6185 this.indent.increase();
6186 }
6187 else {
6188 this.indent.decrease();
6189 }
6190 }
6191
6192 return false;
6193 },
6194 setupSelectAll: function (e, key) {
6195 if (this.keydown.ctrl && key === 65) {
6196 this.utils.enableSelectAll();
6197 }
6198 else if (key !== this.keyCode.LEFT_WIN && !this.keydown.ctrl) {
6199 this.utils.disableSelectAll();
6200 }
6201 },
6202 insertNewLine: function (e) {
6203 e.preventDefault();
6204
6205 var node = document.createTextNode('\n');
6206
6207 var sel = this.selection.get();
6208 var range = this.selection.range(sel);
6209
6210 range.deleteContents();
6211 range.insertNode(node);
6212
6213 this.caret.after(node);
6214
6215 return false;
6216 },
6217 insertParagraph: function (e) {
6218 e.preventDefault();
6219
6220 var p = document.createElement('p');
6221 //p.innerHTML = this.opts.invisibleSpace;
6222 p.innerHTML = '<br>';
6223
6224 var sel = this.selection.get();
6225 var range = this.selection.range(sel);
6226
6227 range.deleteContents();
6228 range.insertNode(p);
6229
6230 this.caret.start(p);
6231
6232 return false;
6233 },
6234 insertBreakLine: function (e) {
6235 return this.keydown.insertBreakLineProcessing(e);
6236 },
6237 insertDblBreakLine: function (e) {
6238 return this.keydown.insertBreakLineProcessing(e, true);
6239 },
6240 insertBreakLineProcessing: function (e, dbl) {
6241 e.stopPropagation();
6242
6243 var br1 = document.createElement('br');
6244 this.insert.node(br1);
6245
6246 if (dbl === true) {
6247 var br2 = document.createElement('br');
6248 this.insert.node(br2);
6249 this.caret.after(br2);
6250 }
6251 else {
6252 this.caret.after(br1);
6253 }
6254
6255 return false;
6256
6257 },
6258 wrapToParagraph: function () {
6259 var $current = $(this.keydown.current);
6260 var node = $('<p>').append($current.clone());
6261 $current.replaceWith(node);
6262
6263 var next = $(node).next();
6264 if (typeof (next[0]) !== 'undefined' && next[0].tagName === 'BR') {
6265 next.remove();
6266 }
6267
6268 this.caret.end(node);
6269
6270 },
6271 replaceToParagraph: function (tag) {
6272 var blockElem = this.selection.block();
6273 var $prev = $(blockElem).prev();
6274
6275 var blockHtml = blockElem.innerHTML.replace(/<br\s?\/?>/gi, '');
6276 if (blockElem.tagName === tag && this.utils.isEmpty(blockHtml) && !$(blockElem).hasClass(
6277 'redactor-in')) {
6278 var p = document.createElement('p');
6279 $(blockElem).replaceWith(p);
6280
6281 this.keydown.setCaretToParagraph(p);
6282
6283 return false;
6284 }
6285 else if (blockElem.tagName === 'P') {
6286 $(blockElem).removeAttr('class').removeAttr('style');
6287
6288 // fix #227
6289 if (this.detect.isIe() && this.utils.isEmpty(blockHtml) && this.utils.isInline(
6290 this.keydown.parent)) {
6291 $(blockElem).on('input', $.proxy(function () {
6292 var parent = this.selection.parent();
6293 if (this.utils.isInline(parent)) {
6294 var html = $(parent).html();
6295 $(blockElem).html(html);
6296 this.caret.end(blockElem);
6297 }
6298
6299 $(blockElem).off('keyup');
6300
6301 }, this));
6302 }
6303
6304 return false;
6305 }
6306 else if ($prev.hasClass(this.opts.videoContainerClass)) {
6307 $prev.removeAttr('class');
6308
6309 var p = document.createElement('p');
6310 $prev.replaceWith(p);
6311
6312 this.keydown.setCaretToParagraph(p);
6313
6314 return false;
6315 }
6316 },
6317 setCaretToParagraph: function (p) {
6318 var range = document.createRange();
6319 range.setStart(p, 0);
6320
6321 var textNode = document.createTextNode('\u200B');
6322
6323 range.insertNode(textNode);
6324 range.setStartAfter(textNode);
6325 range.collapse(true);
6326
6327 var sel = window.getSelection();
6328 sel.removeAllRanges();
6329 sel.addRange(range);
6330 },
6331 removeInvisibleSpace: function () {
6332 var $current = $(this.keydown.current);
6333 if ($current.text().search(/^\u200B$/g) === 0) {
6334 $current.remove();
6335 }
6336 },
6337 removeEmptyListInTable: function (e) {
6338 var $current = $(this.keydown.current);
6339 var $parent = $(this.keydown.parent);
6340 var td = $current.closest('td', this.$editor[0]);
6341
6342 if (td.length !== 0 && $current.closest('li',
6343 this.$editor[0]
6344 ) && $parent.children('li').length === 1) {
6345 if (!this.utils.isEmpty($current.text())) {
6346 return;
6347 }
6348
6349 e.preventDefault();
6350
6351 $current.remove();
6352 $parent.remove();
6353
6354 this.caret.start(td);
6355 }
6356 },
6357 removeEmptyLists: function () {
6358 var removeIt = function () {
6359 var html = $.trim(this.innerHTML).replace(/\/t\/n/g, '');
6360 if (html === '') {
6361 $(this).remove();
6362 }
6363 };
6364
6365 this.core.editor().find('li').each(removeIt);
6366 this.core.editor().find('ul, ol').each(removeIt);
6367 },
6368 formatEmpty: function (e) {
6369 var html = $.trim(this.core.editor().html());
6370
6371 if (!this.utils.isEmpty(html)) {
6372 return;
6373 }
6374
6375 e.preventDefault();
6376
6377 if (this.opts.type === 'inline' || this.opts.type === 'pre') {
6378 this.core.editor().html(this.marker.html());
6379 this.selection.restore();
6380 }
6381 else {
6382 var updateHtml = function() {
6383 this.core.editor().html(this.opts.emptyHtml);
6384 this.focus.start();
6385 }.bind(this);
6386
6387 if (Environment !== null && Environment.platform() === 'ios') {
6388 // In iOS Safari the backspace sometimes appears to be triggered twice if the editor
6389 // is completely empty. After debugging for way too much time, and realizing that
6390 // the remote debugger's breakpoints alter the behavior of async callbacks (*), this
6391 // should solve the issue.
6392 //
6393 // (*) Set up a `console.log()` inside a MutationObserver and then make use of the
6394 // `debugger;` statement to halt the execution flow. The observer is executed, but
6395 // the output never appears on the console. Output works if there is no breakpoint.
6396 setTimeout(updateHtml, 50);
6397 }
6398 else {
6399 updateHtml();
6400 }
6401 }
6402
6403 return false;
6404
6405 }
6406 };
6407 },
6408
6409 // =keyup
6410 keyup: function () {
6411 return {
6412 init: function (e) {
6413 if (this.rtePaste) {
6414 return;
6415 }
6416
6417 var key = e.which;
6418 this.keyup.block = this.selection.block();
6419 this.keyup.current = this.selection.current();
6420 this.keyup.parent = this.selection.parent();
6421 this.keyup.lastShiftKey = e.shiftKey;
6422
6423 // callback
6424 var stop = this.core.callback('keyup', e);
6425 if (stop === false) {
6426 e.preventDefault();
6427 return false;
6428 }
6429
6430 // replace a prev figure to paragraph if caret is before image
6431 if (key === this.keyCode.ENTER) {
6432 if (this.keyup.block && this.keyup.block.tagName === 'FIGURE') {
6433 var $prev = $(this.keyup.block).prev();
6434 if ($prev.length !== 0 && $prev[0].tagName === 'FIGURE') {
6435 var $newTag = this.utils.replaceToTag($prev, 'p');
6436 this.caret.start($newTag);
6437 return;
6438 }
6439 }
6440 }
6441
6442 // replace figure to paragraph
6443 if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) {
6444 if (this.utils.isSelectAll()) {
6445 this.focus.start();
6446
6447 return;
6448 }
6449
6450 // if caret before figure - delete image
6451 if (this.keyup.block && this.keydown.block && this.keyup.block.tagName === 'FIGURE' && this.utils.isStartOfElement(
6452 this.keydown.block)) {
6453 e.preventDefault();
6454
6455 this.selection.save();
6456 $(this.keyup.block).find('figcaption').remove();
6457 $(this.keyup.block).find('img').first().remove();
6458 this.utils.replaceToTag(this.keyup.block, 'p');
6459
6460 var $marker = this.marker.find();
6461 $('html, body').animate({scrollTop: $marker.position().top + 20},
6462 500
6463 );
6464
6465 this.selection.restore();
6466 return;
6467 }
6468
6469 // if paragraph does contain only image replace to figure
6470 if (this.keyup.block && this.keyup.block.tagName === 'P') {
6471 var isContainImage = $(this.keyup.block).find('img').length;
6472 var text = $(this.keyup.block).text().replace(/\u200B/g, '');
6473 if (text === '' && isContainImage !== 0) {
6474 this.utils.replaceToTag(this.keyup.block, 'figure');
6475 }
6476 }
6477
6478 // if figure does not contain image - replace to paragraph
6479 if (this.keyup.block && this.keyup.block.tagName === 'FIGURE' && $(this.keyup.block).find(
6480 'img').length === 0) {
6481 this.selection.save();
6482 this.utils.replaceToTag(this.keyup.block, 'p');
6483 this.selection.restore();
6484 }
6485 }
6486 }
6487
6488 };
6489 },
6490
6491 // =lang
6492 lang: function () {
6493 return {
6494 load: function () {
6495 this.opts.curLang = this.opts.langs[this.opts.lang];
6496 },
6497 get: function (name) {
6498 return (typeof this.opts.curLang[name] !== 'undefined') ? this.opts.curLang[name] : '';
6499 }
6500 };
6501 },
6502
6503 // =line
6504 line: function () {
6505 return {
6506 insert: function () {
6507 this.buffer.set();
6508
6509 // insert
6510 this.insert.html(this.line.getLineHtml());
6511
6512 // find
6513 var $hr = this.core.editor().find('#redactor-hr-tmp-id');
6514 $hr.removeAttr('id');
6515
6516 this.core.callback('insertedLine', $hr);
6517
6518 return $hr;
6519 },
6520 getLineHtml: function () {
6521 var html = '<hr id="redactor-hr-tmp-id" />';
6522 if (!this.detect.isFirefox() && this.utils.isEmpty()) {
6523 html += '<p>' + this.opts.emptyHtml + '</p>';
6524 }
6525
6526 return html;
6527 }, // ff only
6528 removeOnBackspace: function (e) {
6529 if (!this.utils.isCollapsed()) {
6530 return;
6531 }
6532
6533 var $block = $(this.selection.block());
6534 if ($block.length === 0 || !this.utils.isStartOfElement($block)) {
6535 return;
6536 }
6537
6538 // if hr is previous element
6539 var $prev = $block.prev();
6540 if ($prev && $prev.length !== 0 && $prev[0].tagName === 'HR') {
6541 e.preventDefault();
6542 $prev.remove();
6543 }
6544 }
6545 };
6546 },
6547
6548 // =link
6549 link: function () {
6550 return {
6551
6552 // public
6553 get: function () {
6554 return $(this.selection.inlines('a'));
6555 },
6556 is: function () {
6557 var nodes = this.selection.nodes();
6558 var $link = $(this.selection.current()).closest('a', this.core.editor()[0]);
6559
6560 return ($link.length === 0 || nodes.length > 1) ? false : $link;
6561 },
6562 unlink: function (e) {
6563 // if call from clickable element
6564 if (typeof e !== 'undefined' && e.preventDefault) {
6565 e.preventDefault();
6566 }
6567
6568 // buffer
6569 this.buffer.set();
6570
6571 var links = this.selection.inlines('a');
6572 if (links.length === 0) {
6573 return;
6574 }
6575
6576 var $links = this.link.replaceLinksToText(links);
6577
6578 this.observe.closeAllTooltip();
6579 this.core.callback('deletedLink', $links);
6580
6581 },
6582 insert: function (link, cleaned) {
6583 var $el = this.link.is();
6584
6585 if (cleaned !== true) {
6586 link = this.link.buildLinkFromObject($el, link);
6587 if (link === false) {
6588 return false;
6589 }
6590 }
6591
6592 // buffer
6593 this.buffer.set();
6594
6595 // callback
6596 link = this.core.callback('beforeInsertingLink', link);
6597
6598 if ($el === false) {
6599 // insert
6600 $el = $('<a />');
6601 $el = this.link.update($el, link);
6602 $el = $(this.insert.node($el));
6603
6604 var $parent = $el.parent();
6605 if (this.utils.isRedactorParent($parent) === false) {
6606 $el.wrap('<p>');
6607 }
6608
6609 // remove unlink wrapper
6610 if ($parent.hasClass('redactor-unlink')) {
6611 $parent.replaceWith(function () {
6612 return $(this).contents();
6613 });
6614 }
6615
6616 this.caret.after($el);
6617 this.core.callback('insertedLink', $el);
6618 }
6619 else {
6620 // update
6621 $el = this.link.update($el, link);
6622 this.caret.after($el);
6623 }
6624
6625 return $el;
6626
6627 },
6628 update: function ($el, link) {
6629 $el.text(link.text);
6630 $el.attr('href', link.url);
6631
6632 this.link.target($el, link.target);
6633
6634 return $el;
6635
6636 },
6637 target: function ($el, target) {
6638 return (target) ? $el.attr('target', '_blank') : $el.removeAttr('target');
6639 },
6640 show: function (e) {
6641 // if call from clickable element
6642 if (typeof e !== 'undefined' && e.preventDefault) {
6643 e.preventDefault();
6644 }
6645
6646 // close tooltip
6647 this.observe.closeAllTooltip();
6648
6649 // is link
6650 var $el = this.link.is();
6651
6652 // build modal
6653 this.link.buildModal($el);
6654
6655 // build link
6656 var link = this.link.buildLinkFromElement($el);
6657
6658 // if link cut & paste inside editor browser added self host to a link
6659 link.url = this.link.removeSelfHostFromUrl(link.url);
6660
6661 // new tab target
6662 if (this.opts.linkNewTab && !$el) {
6663 link.target = true;
6664 }
6665
6666 // set modal values
6667 this.link.setModalValues(link);
6668
6669 // show modal
6670 this.modal.show();
6671
6672 // focus
6673 if (this.detect.isDesktop()) {
6674 $('#redactor-link-url').focus();
6675 }
6676 },
6677
6678 // private
6679 setModalValues: function (link) {
6680 $('#redactor-link-blank').prop('checked', link.target);
6681 $('#redactor-link-url').val(link.url);
6682 $('#redactor-link-url-text').val(link.text);
6683 },
6684 buildModal: function ($el) {
6685 this.modal.load('link',
6686 this.lang.get(($el === false) ? 'link-insert' : 'link-edit'),
6687 600
6688 );
6689
6690 // button insert
6691 var $btn = this.modal.getActionButton();
6692 $btn.text(this.lang.get(($el === false) ? 'insert' : 'save')).on('click',
6693 $.proxy(this.link.callback, this)
6694 );
6695
6696 },
6697 callback: function () {
6698 // build link
6699 var link = this.link.buildLinkFromModal();
6700 if (link === false) {
6701 return false;
6702 }
6703
6704 // close
6705 this.modal.close();
6706
6707 // insert or update
6708 this.link.insert(link, true);
6709 },
6710 cleanUrl: function (url) {
6711 return (typeof url === 'undefined') ? '' : $.trim(url.replace(/[^\W\w\D\d+&\'@#/%?=~_|!:,.;\(\)]/gi,
6712 ''
6713 ));
6714 },
6715 cleanText: function (text) {
6716 return (typeof text === 'undefined') ? '' : $.trim(text.replace(/(<([^>]+)>)/gi,
6717 ''
6718 ));
6719 },
6720 getText: function (link) {
6721 return (link.text === '' && link.url !== '') ? this.link.truncateUrl(link.url.replace(/<|>/g,
6722 ''
6723 )) : link.text;
6724 },
6725 isUrl: function (url) {
6726 var reUrl = new RegExp(
6727 '^((https?|ftp):\\/\\/)?(([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$',
6728 'i'
6729 );
6730
6731 return (reUrl.test(url)) ? url : false;
6732 },
6733 isMailto: function (url) {
6734 return (url.search('@') !== -1 && /(http|ftp|https):\/\//i.test(url) === false);
6735 },
6736 isEmpty: function (link) {
6737 return (link.url === '' || (link.text === '' && link.url === ''));
6738 },
6739 truncateUrl: function (url) {
6740 return (url.length > this.opts.linkSize) ? url.substring(0,
6741 this.opts.linkSize
6742 ) + '...' : url;
6743 },
6744 parse: function (link) {
6745 // mailto
6746 if (this.link.isMailto(link.url)) {
6747 link.url = 'mailto:' + link.url.replace('mailto:', '');
6748 }
6749 // url
6750 else if (link.url.search('#') !== 0) {
6751 if (this.opts.linkValidation) {
6752 link.url = (this.link.isUrl(link.url)) ? 'http://' + link.url.replace(/(ftp|https?):\/\//gi,
6753 ''
6754 ) : link.url;
6755 }
6756 }
6757
6758 // empty url or text or isn't url
6759 return (this.link.isEmpty(link) || link.url === false) ? false : link;
6760
6761 },
6762 buildLinkFromModal: function () {
6763 var link = {};
6764
6765 // url
6766 link.url = this.link.cleanUrl($('#redactor-link-url').val());
6767
6768 // text
6769 link.text = this.link.cleanText($('#redactor-link-url-text').val());
6770 link.text = this.link.getText(link);
6771
6772 // target
6773 link.target = ($('#redactor-link-blank').prop('checked')) ? true : false;
6774
6775 // parse
6776 return this.link.parse(link);
6777
6778 },
6779 buildLinkFromObject: function ($el, link) {
6780 // url
6781 link.url = this.link.cleanUrl(link.url);
6782
6783 // text
6784 link.text = (typeof link.text === 'undefined' && this.selection.is()) ? this.selection.text() : this.link.cleanText(
6785 link.text);
6786 link.text = this.link.getText(link);
6787
6788 // target
6789 link.target = ($el === false) ? link.target : this.link.buildTarget($el);
6790
6791 // parse
6792 return this.link.parse(link);
6793
6794 },
6795 buildLinkFromElement: function ($el) {
6796 var link = {
6797 url: '',
6798 text: (this.selection.is()) ? this.selection.text() : '',
6799 target: false
6800 };
6801
6802 if ($el !== false) {
6803 link.url = $el.attr('href');
6804 link.text = $el.text();
6805 link.target = this.link.buildTarget($el);
6806 }
6807
6808 return link;
6809 },
6810 buildTarget: function ($el) {
6811 return (typeof $el.attr('target') !== 'undefined' && $el.attr('target') === '_blank') ? true : false;
6812 },
6813 removeSelfHostFromUrl: function (url) {
6814 var href = self.location.href.replace('#', '').replace(/\/$/i, '');
6815 return url.replace(/^\/\#/, '#').replace(href, '').replace('mailto:', '');
6816 },
6817 replaceLinksToText: function (links) {
6818 var $first;
6819 var $links = $.each(links, function (i, s) {
6820 var $el = $(s);
6821 var $unlinked = $('<span class="redactor-unlink" />').append($el.contents());
6822 $el.replaceWith($unlinked);
6823
6824 if (i === 0) {
6825 $first = $unlinked;
6826 }
6827
6828 return $el;
6829 });
6830
6831 // set caret after unlinked node
6832 if (links.length === 1 && this.selection.isCollapsed()) {
6833 this.caret.after($first);
6834 }
6835
6836 return $links;
6837 }
6838 };
6839 },
6840
6841 // =linkify -- UNSUPPORTED MODULE
6842 linkify: function () {
6843 return {
6844 isKey: function () {},
6845 isLink: function () {},
6846 isFiltered: function () {},
6847 handler: function () {},
6848 format: function () {},
6849 convertVideoLinks: function () {},
6850 convertImages: function () {},
6851 convertLinks: function () {}
6852 };
6853 },
6854
6855 // =list
6856 list: function () {
6857 return {
6858 toggle: function (type) {
6859 if (this.utils.inBlocks(['table', 'td', 'th', 'tr'])) {
6860 return;
6861 }
6862
6863 type = (type === 'orderedlist') ? 'ol' : type;
6864 type = (type === 'unorderedlist') ? 'ul' : type;
6865
6866 type = type.toLowerCase();
6867
6868 this.buffer.set();
6869 this.selection.save();
6870
6871 var nodes = this.list._getBlocks();
6872 var block = this.selection.block();
6873 var $list = $(block).parents('ul, ol').last();
6874 if (nodes.length === 0 && $list.length !== 0) {
6875 nodes = [$list.get(0)];
6876 }
6877
6878 nodes = (this.list._isUnformat(type, nodes)) ? this.list._unformat(type,
6879 nodes
6880 ) : this.list._format(type, nodes);
6881
6882 this.selection.restore();
6883
6884 return nodes;
6885 },
6886 get: function () {
6887 var current = this.selection.current();
6888 var $list = $(current).closest('ul, ol', this.core.editor()[0]);
6889
6890 return ($list.length === 0) ? false : $list;
6891 },
6892 combineAfterAndBefore: function (block) {
6893 var $prev = $(block).prev();
6894 var $next = $(block).next();
6895 var isEmptyBlock = (block && block.tagName === 'P' && (block.innerHTML === '<br>' || block.innerHTML === ''));
6896 var isBlockWrapped = ($prev.closest('ol, ul',
6897 this.core.editor()[0]
6898 ).length === 1 && $next.closest(
6899 'ol, ul',
6900 this.core.editor()[0]
6901 ).length === 1);
6902
6903 if (isEmptyBlock && isBlockWrapped) {
6904 $prev.children('li').last().append(this.marker.get());
6905 $prev.append($next.contents());
6906 this.selection.restore();
6907
6908 return true;
6909 }
6910
6911 return false;
6912
6913 },
6914 _getBlocks: function () {
6915 var finalBlocks = [];
6916 var blocks = this.selection.blocks();
6917 for (var i = 0; i < blocks.length; i++) {
6918 var $el = $(blocks[i]);
6919 var isFirst = ($el.parent().hasClass('redactor-in'));
6920
6921 if (isFirst) finalBlocks.push(blocks[i]);
6922 }
6923
6924 return finalBlocks;
6925 },
6926 _isUnformat: function (type, nodes) {
6927 var countLists = 0;
6928 for (var i = 0; i < nodes.length; i++) {
6929 if (nodes[i].nodeType !== 3) {
6930 var tag = nodes[i].tagName.toLowerCase();
6931 if (tag === type || tag === 'figure') {
6932 countLists++;
6933 }
6934 }
6935 }
6936
6937 return (countLists === nodes.length);
6938 },
6939 _uniteBlocks: function (nodes, tags) {
6940 var z = 0;
6941 var blocks = {0: []};
6942 var lastcell = false;
6943 for (var i = 0; i < nodes.length; i++) {
6944 var $node = $(nodes[i]);
6945 var $cell = $node.closest('th, td');
6946
6947 if ($cell.length !== 0) {
6948 if ($cell.get(0) !== lastcell) {
6949 // create block
6950 z++;
6951 blocks[z] = [];
6952 }
6953
6954 if (this.list._isUniteBlock(nodes[i], tags)) {
6955 blocks[z].push(nodes[i]);
6956 }
6957 }
6958 else {
6959 if (this.list._isUniteBlock(nodes[i], tags)) {
6960 blocks[z].push(nodes[i]);
6961 }
6962 else {
6963 // create block
6964 z++;
6965 blocks[z] = [];
6966 }
6967 }
6968
6969 lastcell = $cell.get();
6970 }
6971
6972 return blocks;
6973 },
6974 _isUniteBlock: function (node, tags) {
6975 return (node.nodeType === 3 || tags.indexOf(node.tagName.toLowerCase()) !== -1);
6976 },
6977 _createList: function (type, blocks, key) {
6978 var last = blocks[blocks.length - 1];
6979 var $last = $(last);
6980 var $list = $('<' + type + '>');
6981 $last.after($list);
6982
6983 return $list;
6984 },
6985 _createListItem: function (item) {
6986 var $item = $('<li>');
6987 if (item.nodeType === 3) {
6988 $item.append(item);
6989 }
6990 else {
6991 var $el = $(item);
6992 $item.append($el.contents());
6993 $el.remove();
6994 }
6995
6996 return $item;
6997 },
6998 _format: function (type, nodes) {
6999 var tags = [
7000 'p',
7001 'div',
7002 'blockquote',
7003 'pre',
7004 'h1',
7005 'h2',
7006 'h3',
7007 'h4',
7008 'h5',
7009 'h6',
7010 'ul',
7011 'ol'
7012 ];
7013 var blocks = this.list._uniteBlocks(nodes, tags);
7014 var lists = [];
7015
7016 for (var key in blocks) {
7017 var items = blocks[key];
7018 var $list = this.list._createList(type, blocks[key]);
7019
7020 for (var i = 0; i < items.length; i++) {
7021 var $item;
7022
7023 // lists
7024 if (items[i].nodeType !== 3 && (items[i].tagName === 'UL' || items[i].tagName === 'OL')) {
7025 $item = $(items[i]).contents();
7026 $list.append($item);
7027 }
7028 // other blocks or texts
7029 else {
7030 $item = this.list._createListItem(items[i]);
7031 //this.utils.normalizeTextNodes($item);
7032 $list.append($item);
7033 }
7034 }
7035
7036 lists.push($list.get(0));
7037 }
7038
7039 return lists;
7040 },
7041 _unformat: function (type, nodes) {
7042
7043 if (nodes.length === 1) {
7044 // one list
7045 var $list = $(nodes[0]);
7046 var $items = $list.find('li');
7047
7048 var selectedItems = this.selection.blocks(['li']);
7049 var block = this.selection.block();
7050 var $li = $(block).closest('li');
7051 if (selectedItems.length === 0 && $li.length !== 0) {
7052 selectedItems = [$li.get(0)];
7053 }
7054
7055 // 1) entire
7056 if (selectedItems.length === $items.length) {
7057 return this.list._unformatEntire(nodes[0]);
7058 }
7059
7060 var pos = this.list._getItemsPosition($items, selectedItems);
7061
7062 // 2) top
7063 if (pos === 'Top') {
7064 return this.list._unformatAtSide('before',
7065 selectedItems,
7066 $list
7067 );
7068 }
7069
7070 // 3) bottom
7071 else if (pos === 'Bottom') {
7072 selectedItems.reverse();
7073 return this.list._unformatAtSide('after', selectedItems, $list);
7074 }
7075
7076 // 4) middle
7077 else if (pos === 'Middle') {
7078 var $last = $(selectedItems[selectedItems.length - 1]);
7079
7080 var ci = false;
7081
7082 var $parent = false;
7083 var $secondList = $('<' + $list.get(0).tagName.toLowerCase() + '>');
7084 $items.each(function (i, node) {
7085 if (ci) {
7086 var $node = $(node);
7087 var $childList = ($node.children('ul, ol').length !== 0);
7088
7089 if ($node.closest('.redactor-split-item').length === 0 && ($parent === false || $node.closest(
7090 $parent).length === 0)) {
7091 $node.addClass('redactor-split-item');
7092 }
7093
7094 $parent = $node;
7095
7096 }
7097
7098 if (node === $last.get(0)) {
7099 ci = true;
7100 }
7101 });
7102
7103 $items.filter('.redactor-split-item').each(function (i, node) {
7104 var $node = $(node);
7105 $node.removeClass('redactor-split-item');
7106 $secondList.append(node);
7107 });
7108
7109 $list.after($secondList);
7110
7111 selectedItems.reverse();
7112 for (var i = 0; i < selectedItems.length; i++) {
7113 var $item = $(selectedItems[i]);
7114 var $container = this.list._createUnformatContainer(
7115 $item);
7116
7117 $list.after($container);
7118 $container.find('ul, ol').remove();
7119 $item.remove();
7120 }
7121
7122 return;
7123 }
7124
7125 }
7126 else {
7127 // unformat all
7128 for (var i = 0; i < nodes.length; i++) {
7129 if (nodes[i].nodeType !== 3 && nodes[i].tagName.toLowerCase() === type) {
7130 this.list._unformatEntire(nodes[i]);
7131 }
7132 }
7133 }
7134 },
7135 _unformatEntire: function (list) {
7136 var $list = $(list);
7137 var $items = $list.find('li');
7138 $items.each(function (i, node) {
7139 var $item = $(node);
7140 var $container = this.list._createUnformatContainer($item);
7141
7142 $item.remove();
7143 $list.before($container);
7144
7145 }.bind(this));
7146
7147 $list.remove();
7148 },
7149 _unformatAtSide: function (type, selectedItems, $list) {
7150 for (var i = 0; i < selectedItems.length; i++) {
7151 var $item = $(selectedItems[i]);
7152 var $container = this.list._createUnformatContainer($item);
7153
7154 $list[type]($container);
7155
7156 var $innerLists = $container.find('ul, ol').first();
7157 $item.append($innerLists);
7158
7159 $innerLists.each(function (i, node) {
7160 var $node = $(node);
7161 var $parent = $node.closest('li');
7162
7163 if ($parent.get(0) === selectedItems[i]) {
7164 $node.unwrap();
7165 $parent.addClass('r-unwrapped');
7166 }
7167
7168 });
7169
7170 if (this.utils.isEmpty($item.html())) $item.remove();
7171 }
7172
7173 // clear empty
7174 $list.find('.r-unwrapped').each(function (node) {
7175 var $node = $(node);
7176 if ($node.html().trim() === '') {
7177 $node.remove();
7178 }
7179 else {
7180 $node.removeClass('r-unwrapped');
7181 }
7182 });
7183 },
7184 _getItemsPosition: function ($items, selectedItems) {
7185 var pos = 'Middle';
7186
7187 var sFirst = selectedItems[0];
7188 var sLast = selectedItems[selectedItems.length - 1];
7189
7190 var first = $items.first().get(0);
7191 var last = $items.last().get(0);
7192
7193 if (first === sFirst && last !== sLast) {
7194 pos = 'Top';
7195 }
7196 else if (first !== sFirst && last === sLast) {
7197 pos = 'Bottom';
7198 }
7199
7200 return pos;
7201 },
7202 _createUnformatContainer: function ($item) {
7203 var $container = $('<p>');
7204 $container.append($item.contents());
7205
7206 return $container;
7207 }
7208 };
7209 },
7210
7211 // =marker
7212 marker: function () {
7213 return {
7214
7215 // public
7216 get: function (num) {
7217 num = (typeof num === 'undefined') ? 1 : num;
7218
7219 var marker = document.createElement('span');
7220
7221 marker.id = 'selection-marker-' + num;
7222 marker.className = 'redactor-selection-marker';
7223 marker.innerHTML = this.opts.invisibleSpace;
7224
7225 return marker;
7226 },
7227 html: function (num) {
7228 return this.utils.getOuterHtml(this.marker.get(num));
7229 },
7230 find: function (num) {
7231 num = (typeof num === 'undefined') ? 1 : num;
7232
7233 return this.core.editor().find('span#selection-marker-' + num);
7234 },
7235 insert: function () {
7236 var sel = this.selection.get();
7237 var range = this.selection.range(sel);
7238
7239 this.marker.insertNode(range, this.marker.get(1), true);
7240 if (range && range.collapsed === false) {
7241 this.marker.insertNode(range, this.marker.get(2), false);
7242 }
7243
7244 },
7245 remove: function () {
7246 this.core.editor().find('.redactor-selection-marker').each(this.marker.iterateRemove);
7247 },
7248
7249 // private
7250 insertNode: function (range, node, collapse) {
7251 var parent = this.selection.parent();
7252 if (range === null || $(parent).closest('.redactor-in').length === 0) {
7253 return;
7254 }
7255
7256 range = range.cloneRange();
7257
7258 try {
7259 range.collapse(collapse);
7260 range.insertNode(node);
7261 }
7262 catch (e) {
7263 this.focus.start();
7264 }
7265 },
7266 iterateRemove: function (i, el) {
7267 var $el = $(el);
7268 var text = $el.text().replace(/\u200B/g, '');
7269 var parent = $el.parent()[0];
7270
7271 if (text === '') $el.remove(); else $el.replaceWith(function () { return $(this).contents(); });
7272
7273 // if (parent && parent.normalize) parent.normalize();
7274 }
7275 };
7276 },
7277
7278 // =modal
7279 modal: function () {
7280 return {
7281 callbacks: {},
7282 templates: function () {
7283 this.opts.modal = {
7284 'image-edit': '',
7285
7286 'image': '',
7287
7288 'file': '',
7289
7290 '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(
7291 'text') + '</label>' + '<input type="text" id="redactor-link-url-text" aria-label="' + this.lang.get(
7292 'text') + '" />' + '</section>' + '<section>' + '<label class="checkbox"><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get(
7293 'link-in-new-tab') + '</label>' + '</section>' + '<section>' + '<button id="redactor-modal-button-action">' + this.lang.get(
7294 'insert') + '</button>' + '<button id="redactor-modal-button-cancel">' + this.lang.get(
7295 'cancel') + '</button>' + '</section>' + '</div>'
7296 };
7297
7298 $.extend(this.opts, this.opts.modal);
7299
7300 },
7301 addCallback: function (name, callback) {
7302 this.modal.callbacks[name] = callback;
7303 },
7304 addTemplate: function (name, template) {
7305 this.opts.modal[name] = template;
7306 },
7307 getTemplate: function (name) {
7308 return this.opts.modal[name];
7309 },
7310 getModal: function () {
7311 return this.$modalBody;
7312 },
7313 getActionButton: function () {
7314 return this.$modalBody.find('#redactor-modal-button-action');
7315 },
7316 getCancelButton: function () {
7317 return this.$modalBody.find('#redactor-modal-button-cancel');
7318 },
7319 getDeleteButton: function () {
7320 return this.$modalBody.find('#redactor-modal-button-delete');
7321 },
7322 load: function () { /* WoltLabModal.js */ },
7323 show: function () { /* WoltLabModal.js */ },
7324 buildWidth: function () { },
7325 buildTabber: function () {},
7326 showTab: function () {},
7327 setTitle: function () { /* WoltLabModal.js */ },
7328 setContent: function () {
7329 this.$modalBody.html(this.modal.getTemplate(this.modal.templateName));
7330
7331 this.modal.getCancelButton().on('mousedown', $.proxy(this.modal.close, this));
7332 },
7333 setDraggable: function () {},
7334 setEnter: function () {},
7335 build: function () {
7336 this.modal.buildOverlay();
7337
7338 this.$modalBox = $('<div id="redactor-modal-box"/>').hide();
7339 this.$modal = $('<div id="redactor-modal" role="dialog" />');
7340 this.$modalHeader = $('<div id="redactor-modal-header" />');
7341 this.$modalClose = $(
7342 '<button type="button" id="redactor-modal-close" aria-label="' + this.lang.get(
7343 'close') + '" />').html('&times;');
7344 this.$modalBody = $('<div id="redactor-modal-body" />');
7345
7346 this.$modal.append(this.$modalHeader);
7347 this.$modal.append(this.$modalBody);
7348 this.$modal.append(this.$modalClose);
7349 this.$modalBox.append(this.$modal);
7350 this.$modalBox.appendTo(document.body);
7351
7352 },
7353 buildOverlay: function () {
7354 this.$modalOverlay = $('<div id="redactor-modal-overlay">').hide();
7355 $('body').prepend(this.$modalOverlay);
7356 },
7357 enableEvents: function () {},
7358 disableEvents: function () {},
7359 closeHandler: function () {},
7360 close: function () { /* WoltLabModal.js */ }
7361 };
7362 },
7363
7364 // =observe
7365 observe: function () {
7366 return {
7367 load: function () {
7368 if (typeof this.opts.destroyed !== 'undefined') {
7369 return;
7370 }
7371
7372 this.observe.links();
7373 this.observe.images();
7374
7375 },
7376 isCurrent: function ($el, $current) {
7377 if (typeof $current === 'undefined') {
7378 $current = $(this.selection.current());
7379 }
7380
7381 return $current.is($el) || $current.parents($el).length > 0;
7382 },
7383 toolbar: function () {
7384 this.observe.buttons();
7385 this.observe.dropdowns();
7386 },
7387 buttons: function (e, btnName) {
7388 var current = this.selection.current();
7389 var parent = this.selection.parent();
7390
7391 if (e !== false) {
7392 this.button.setInactiveAll();
7393 }
7394 else {
7395 this.button.setInactiveAll(btnName);
7396 }
7397
7398 if (e === false && btnName !== 'html') {
7399 if ($.inArray(btnName, this.opts.activeButtons) !== -1) {
7400 this.button.toggleActive(btnName);
7401 }
7402 return;
7403 }
7404
7405 if (!this.utils.isRedactorParent(current)) {
7406 return;
7407 }
7408
7409 // disable line
7410 if (this.core.editor().css('display') !== 'none') {
7411 if (this.utils.isCurrentOrParentHeader() || this.utils.isCurrentOrParent(
7412 ['table', 'pre', 'blockquote', 'li'])) {
7413 this.button.disable('horizontalrule');
7414 }
7415 else {
7416 this.button.enable('horizontalrule');
7417 }
7418 }
7419
7420 $.each(this.opts.activeButtonsStates, $.proxy(function (key, value) {
7421 var parentEl = $(parent).closest(key, this.$editor[0]);
7422 var currentEl = $(current).closest(key, this.$editor[0]);
7423
7424 if (parentEl.length !== 0 && !this.utils.isRedactorParent(parentEl)) {
7425 return;
7426 }
7427
7428 if (!this.utils.isRedactorParent(currentEl)) {
7429 return;
7430 }
7431
7432 if (parentEl.length !== 0 || currentEl.closest(key,
7433 this.$editor[0]
7434 ).length !== 0) {
7435 this.button.setActive(value);
7436 }
7437
7438 }, this));
7439
7440 },
7441 dropdowns: function () {
7442 var finded = $('<div />').html(this.selection.html()).find('a').length;
7443 var $current = $(this.selection.current());
7444 var isRedactor = this.utils.isRedactorParent($current);
7445
7446 $.each(this.opts.observe.dropdowns, $.proxy(function (key, value) {
7447 var observe = value.observe, element = observe.element,
7448 $item = value.item,
7449 inValues = typeof observe['in'] !== 'undefined' ? observe['in'] : false,
7450 outValues = typeof observe.out !== 'undefined' ? observe.out : false;
7451
7452 if (($current.closest(element).length > 0 && isRedactor) || (element === 'a' && finded !== 0)) {
7453 this.observe.setDropdownProperties($item, inValues, outValues);
7454 }
7455 else {
7456 this.observe.setDropdownProperties($item, outValues, inValues);
7457 }
7458
7459 }, this));
7460 },
7461 setDropdownProperties: function ($item, addProperties, deleteProperties) {
7462 if (deleteProperties && typeof deleteProperties.attr !== 'undefined') {
7463 this.observe.setDropdownAttr($item, deleteProperties.attr, true);
7464 }
7465
7466 if (typeof addProperties.attr !== 'undefined') {
7467 this.observe.setDropdownAttr($item, addProperties.attr);
7468 }
7469
7470 if (typeof addProperties.title !== 'undefined') {
7471 $item.find('span').text(addProperties.title);
7472 }
7473 },
7474 setDropdownAttr: function ($item, properties, isDelete) {
7475 $.each(properties, function (key, value) {
7476 if (key === 'class') {
7477 if (!isDelete) {
7478 $item.addClass(value);
7479 }
7480 else {
7481 $item.removeClass(value);
7482 }
7483 }
7484 else {
7485 if (!isDelete) {
7486 $item.attr(key, value);
7487 }
7488 else {
7489 $item.removeAttr(key);
7490 }
7491 }
7492 });
7493 },
7494 addDropdown: function ($item, btnName, btnObject) {
7495 if (typeof btnObject.observe === 'undefined') {
7496 return;
7497 }
7498
7499 btnObject.item = $item;
7500
7501 this.opts.observe.dropdowns.push(btnObject);
7502 },
7503 images: function () {
7504 if (this.opts.imageEditable) {
7505 this.core.editor().addClass('redactor-layer-img-edit');
7506 this.core.editor().find('img').each($.proxy(function (i, img) {
7507 var $img = $(img);
7508
7509 // IE fix (when we clicked on an image and then press backspace IE does goes to image's url)
7510 $img.closest('a', this.$editor[0]).on('click',
7511 function (e) { e.preventDefault(); }
7512 );
7513
7514 this.image.setEditable($img);
7515
7516 }, this));
7517 }
7518 },
7519 links: function () {
7520 if (this.opts.linkTooltip) {
7521 this.core.editor().find('a').each($.proxy(function (i, s) {
7522 var $link = $(s);
7523 if ($link.data('cached') !== true) {
7524 $link.data('cached', true);
7525 $link.on(
7526 'touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7527 $.proxy(this.observe.showTooltip, this)
7528 );
7529 }
7530
7531 }, this));
7532 }
7533 },
7534 getTooltipPosition: function ($link) {
7535 return $link.offset();
7536 },
7537 showTooltip: function (e) {
7538 var $el = $(e.target);
7539
7540 if ($el[0].tagName === 'IMG') {
7541 return;
7542 }
7543
7544 if ($el[0].tagName !== 'A') {
7545 $el = $el.closest('a', this.$editor[0]);
7546 }
7547
7548 if ($el[0].tagName !== 'A') {
7549 return;
7550 }
7551
7552 var $link = $el;
7553
7554 var pos = this.observe.getTooltipPosition($link);
7555 var tooltip = $('<span class="redactor-link-tooltip"></span>');
7556
7557 var href = $link.attr('href');
7558 if (href === undefined) {
7559 href = '';
7560 }
7561
7562 if (href.length > 24) {
7563 href = href.substring(0, 24) + '...';
7564 }
7565
7566 var aLink = $('<a href="' + $link.attr('href') + '" target="_blank" />').html(
7567 href).addClass('redactor-link-tooltip-action');
7568 var aEdit = $('<a href="#" />').html(this.lang.get('edit')).on('click',
7569 $.proxy(this.link.show, this)
7570 ).addClass('redactor-link-tooltip-action');
7571 var aUnlink = $('<a href="#" />').html(this.lang.get('unlink')).on('click',
7572 $.proxy(this.link.unlink, this)
7573 ).addClass('redactor-link-tooltip-action');
7574
7575 tooltip.append(aLink).append(' | ').append(aEdit).append(' | ').append(aUnlink);
7576
7577 var lineHeight = parseInt($link.css('line-height'), 10);
7578 var lineClicked = Math.ceil((e.pageY - pos.top) / lineHeight);
7579 var top = pos.top + lineClicked * lineHeight;
7580
7581 tooltip.css({
7582 top: top + 'px',
7583 left: pos.left + 'px'
7584 });
7585
7586 $('.redactor-link-tooltip').remove();
7587 $('body').append(tooltip);
7588
7589 this.core.editor().on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7590 $.proxy(this.observe.closeTooltip, this)
7591 );
7592 $(document).on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7593 $.proxy(this.observe.closeTooltip, this)
7594 );
7595 },
7596 closeAllTooltip: function () {
7597 $('.redactor-link-tooltip').remove();
7598 },
7599 closeTooltip: function (e) {
7600 e = e.originalEvent || e;
7601
7602 var target = e.target;
7603 var $parent = $(target).closest('a', this.$editor[0]);
7604 if ($parent.length !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A') {
7605 return;
7606 }
7607 else if ((target.tagName === 'A' && this.utils.isRedactorParent(target)) || $(
7608 target).hasClass('redactor-link-tooltip-action')) {
7609 return;
7610 }
7611
7612 this.observe.closeAllTooltip();
7613
7614 this.core.editor().off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7615 $.proxy(this.observe.closeTooltip, this)
7616 );
7617 $(document).off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid,
7618 $.proxy(this.observe.closeTooltip, this)
7619 );
7620 }
7621
7622 };
7623 },
7624
7625 // =offset
7626 offset: function () {
7627 return {
7628 get: function (node) {
7629 var cloned = this.offset.clone(node);
7630 if (cloned === false) {
7631 return 0;
7632 }
7633
7634 var div = document.createElement('div');
7635 div.appendChild(cloned.cloneContents());
7636 div.innerHTML = div.innerHTML.replace(/<img(.*?[^>])>$/gi, 'i');
7637
7638 var text = $.trim($(div).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
7639 ''
7640 );
7641
7642 return text.length;
7643
7644 },
7645 clone: function (node) {
7646 var sel = this.selection.get();
7647 var range = this.selection.range(sel);
7648
7649 if (range === null && typeof node === 'undefined') {
7650 return false;
7651 }
7652
7653 node = (typeof node === 'undefined') ? this.$editor : node;
7654 if (node === false) {
7655 return false;
7656 }
7657
7658 node = node[0] || node;
7659
7660 var cloned = range.cloneRange();
7661 cloned.selectNodeContents(node);
7662 cloned.setEnd(range.endContainer, range.endOffset);
7663
7664 return cloned;
7665 },
7666 set: function (start, end) {
7667 end = (typeof end === 'undefined') ? start : end;
7668
7669 if (!this.focus.is()) {
7670 this.focus.start();
7671 }
7672
7673 var sel = this.selection.get();
7674 var range = this.selection.range(sel);
7675 var node, offset = 0;
7676 var walker = document.createTreeWalker(this.$editor[0],
7677 NodeFilter.SHOW_TEXT,
7678 null,
7679 null
7680 );
7681
7682 while ((node = walker.nextNode()) !== null) {
7683 offset += node.nodeValue.length;
7684 if (offset > start) {
7685 range.setStart(node, node.nodeValue.length + start - offset);
7686 start = Infinity;
7687 }
7688
7689 if (offset >= end) {
7690 range.setEnd(node, node.nodeValue.length + end - offset);
7691 break;
7692 }
7693 }
7694
7695 range.collapse(false);
7696 this.selection.update(sel, range);
7697 }
7698 };
7699 },
7700
7701 // =paragraphize
7702 paragraphize: function () {
7703 return {
7704 load: function (html) {
7705 if (this.opts.paragraphize === false || this.opts.type === 'inline' || this.opts.type === 'pre') {
7706 return html;
7707 }
7708
7709 if (html === '' || html === '<p></p>') {
7710 return this.opts.emptyHtml;
7711 }
7712
7713 html = html + '\n';
7714
7715 this.paragraphize.safes = [];
7716 this.paragraphize.z = 0;
7717
7718 // before
7719 html = html.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>');
7720 html = html.replace(/<\/pre>/gi, '</pre>\n\n');
7721 html = html.replace(/<p>\s<br><\/p>/gi, '<p></p>');
7722
7723 html = this.paragraphize.getSafes(html);
7724
7725 html = html.replace('<br>', '\n');
7726 html = this.paragraphize.convert(html);
7727
7728 html = this.paragraphize.clear(html);
7729 html = this.paragraphize.restoreSafes(html);
7730
7731 // after
7732 html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts.paragraphizeBlocks.join(
7733 '|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');
7734
7735 return $.trim(html);
7736 },
7737 getSafes: function (html) {
7738 var $div = $('<div />').append(html);
7739
7740 // remove paragraphs in blockquotes
7741 $div.find('blockquote p').replaceWith(function () {
7742 return $(this).append('<br />').contents();
7743 });
7744
7745 $div.find(this.opts.paragraphizeBlocks.join(', ')).each($.proxy(function (i, s) {
7746 this.paragraphize.z++;
7747 this.paragraphize.safes[this.paragraphize.z] = s.outerHTML;
7748
7749 return $(s).replaceWith('\n#####replace' + this.paragraphize.z + '#####\n\n');
7750
7751 }, this));
7752
7753 // deal with redactor selection markers
7754 $div.find('span.redactor-selection-marker').each($.proxy(function (i, s) {
7755 this.paragraphize.z++;
7756 this.paragraphize.safes[this.paragraphize.z] = s.outerHTML;
7757
7758 return $(s).replaceWith('\n#####replace' + this.paragraphize.z + '#####\n\n');
7759 }, this));
7760
7761 return $div.html();
7762 },
7763 restoreSafes: function (html) {
7764 $.each(this.paragraphize.safes, function (i, s) {
7765 s = (typeof s !== 'undefined') ? s.replace(/\$/g, '&#36;') : s;
7766 html = html.replace('#####replace' + i + '#####', s);
7767
7768 });
7769
7770 return html;
7771 },
7772 convert: function (html) {
7773 html = html.replace(/\r\n/g, 'xparagraphmarkerz');
7774 html = html.replace(/\n/g, 'xparagraphmarkerz');
7775 html = html.replace(/\r/g, 'xparagraphmarkerz');
7776
7777 var re1 = /\s+/g;
7778 html = html.replace(re1, ' ');
7779 html = $.trim(html);
7780
7781 var re2 = /xparagraphmarkerzxparagraphmarkerz/gi;
7782 html = html.replace(re2, '</p><p>');
7783
7784 var re3 = /xparagraphmarkerz/gi;
7785 html = html.replace(re3, '<br>');
7786
7787 html = '<p>' + html + '</p>';
7788
7789 html = html.replace('<p></p>', '');
7790 html = html.replace('\r\n\r\n', '');
7791 html = html.replace(/<\/p><p>/g, '</p>\r\n\r\n<p>');
7792 html = html.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7793 html = html.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7794 html = html.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7795 html = html.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7796 html = html.replace(/<p>&nbsp;<\/p>/gi, '');
7797 html = html.replace(/<p>\s?<br>&nbsp;<\/p>/gi, '');
7798 html = html.replace(/<p>\s?<br>/gi, '<p>');
7799
7800 return html;
7801 },
7802 clear: function (html) {
7803
7804 html = html.replace(
7805 /<p>(.*?)#####replace(.*?)#####\s?<\/p>/gi,
7806 '<p>$1</p>#####replace$2#####'
7807 );
7808 html = html.replace(/(<br\s?\/?>){2,}<\/p>/gi, '</p>');
7809
7810 html = html.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>');
7811 html = html.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>');
7812 html = html.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>');
7813 html = html.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>');
7814
7815 html = html.replace(new RegExp('<p><p ', 'gi'), '<p ');
7816 html = html.replace(new RegExp('<p><p>', 'gi'), '<p>');
7817 html = html.replace(new RegExp('</p></p>', 'gi'), '</p>');
7818 html = html.replace(new RegExp('<p>\\s?</p>', 'gi'), '');
7819 html = html.replace(new RegExp('\n</p>', 'gi'), '</p>');
7820 html = html.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>');
7821 html = html.replace(new RegExp('<p>\t*</p>', 'gi'), '');
7822
7823 return html;
7824 }
7825 };
7826 },
7827
7828 // =paste
7829 paste: function () {
7830 return {
7831 init: function (e) {
7832 this.rtePaste = true;
7833 var pre = (this.opts.type === 'pre' || this.utils.isCurrentOrParent('pre')) ? true : false;
7834
7835 // clipboard event
7836 if (this.detect.isDesktop()) {
7837
7838 if (!this.paste.pre && this.opts.clipboardImageUpload && this.opts.imageUpload && this.paste.detectClipboardUpload(
7839 e)) {
7840 if (this.detect.isIe()) {
7841 setTimeout($.proxy(this.paste.clipboardUpload, this),
7842 100
7843 );
7844 }
7845
7846 return;
7847 }
7848 }
7849
7850 this.utils.saveScroll();
7851 this.selection.save();
7852 this.paste.createPasteBox(pre);
7853
7854 $(window).on('scroll.redactor-freeze', $.proxy(function () {
7855 $(window).scrollTop(this.saveBodyScroll);
7856
7857 }, this));
7858
7859 setTimeout($.proxy(function () {
7860 var html = this.paste.getPasteBoxCode(pre);
7861
7862 // buffer
7863 this.buffer.set();
7864 this.selection.restore();
7865
7866 this.utils.restoreScroll();
7867
7868 // paste info
7869 var data = this.clean.getCurrentType(html);
7870
7871 // clean
7872 html = this.clean.onPaste(html, data);
7873
7874 // callback
7875 var returned = this.core.callback('paste', html);
7876 html = (typeof returned === 'undefined') ? html : returned;
7877
7878 this.paste.insert(html, data);
7879 this.rtePaste = false;
7880
7881 // clean pre breaklines
7882 if (pre) {
7883 this.clean.cleanPre();
7884 }
7885
7886 $(window).off('scroll.redactor-freeze');
7887
7888 }, this), 1);
7889
7890 },
7891 getPasteBoxCode: function (pre) {
7892 var html = (pre) ? this.$pasteBox.val() : this.$pasteBox.html();
7893 this.$pasteBox.remove();
7894
7895 return html;
7896 },
7897 createPasteBox: function (pre) {
7898 var css = {
7899 position: 'fixed',
7900 width: '1px',
7901 top: 0,
7902 left: '-9999px'
7903 };
7904
7905 this.$pasteBox = (pre) ? $('<textarea>').css(css) : $('<div>').attr('contenteditable',
7906 'true'
7907 ).css(css);
7908 this.paste.appendPasteBox();
7909 this.$pasteBox.focus();
7910 },
7911 appendPasteBox: function () {
7912 if (this.detect.isIe()) {
7913 this.core.box().append(this.$pasteBox);
7914 }
7915 else {
7916 // bootstrap modal
7917 var $visibleModals = $('.modal-body:visible');
7918 if ($visibleModals.length > 0) {
7919 $visibleModals.append(this.$pasteBox);
7920 }
7921 else {
7922 $('body').prepend(this.$pasteBox);
7923 }
7924 }
7925 },
7926 detectClipboardUpload: function (e) {
7927 e = e.originalEvent || e;
7928
7929 var clipboard = e.clipboardData;
7930 if (this.detect.isIe() || this.detect.isFirefox()) {
7931 return false;
7932 }
7933
7934 // prevent safari fake url
7935 var types = clipboard.types;
7936 if (types.indexOf('public.tiff') !== -1) {
7937 e.preventDefault();
7938 return false;
7939 }
7940
7941 if (!clipboard.items || !clipboard.items.length) {
7942 return;
7943 }
7944
7945 var file = clipboard.items[0].getAsFile();
7946 if (file === null) {
7947 return false;
7948 }
7949
7950 var reader = new FileReader();
7951 reader.readAsDataURL(file);
7952 reader.onload = $.proxy(this.paste.insertFromClipboard, this);
7953
7954 return true;
7955 },
7956 clipboardUpload: function () {
7957 var imgs = this.$editor.find('img');
7958 $.each(imgs, $.proxy(function (i, s) {
7959 if (s.src.search(/^data\:image/i) === -1) {
7960 return;
7961 }
7962
7963 var formData = !!window.FormData ? new FormData() : null;
7964 if (!window.FormData) {
7965 return;
7966 }
7967
7968 this.upload.direct = true;
7969 this.upload.type = 'image';
7970 this.upload.url = this.opts.imageUpload;
7971 this.upload.callback = $.proxy(function (data) {
7972 if (this.detect.isIe()) {
7973 $(s).wrap($('<figure />'));
7974 }
7975
7976 else {
7977 var $parent = $(s).parent();
7978 this.utils.replaceToTag($parent, 'figure');
7979 }
7980
7981 s.src = data.url;
7982 this.core.callback('imageUpload', $(s), data);
7983
7984 }, this);
7985
7986 var blob = this.utils.dataURItoBlob(s.src);
7987
7988 formData.append('clipboard', 1);
7989 formData.append(this.opts.imageUploadParam, blob);
7990
7991 this.upload.send(formData, false);
7992 this.code.sync();
7993 this.rtePaste = false;
7994
7995 }, this));
7996 },
7997 insertFromClipboard: function (e) {
7998 var formData = !!window.FormData ? new FormData() : null;
7999 if (!window.FormData) {
8000 return;
8001 }
8002
8003 this.upload.direct = true;
8004 this.upload.type = 'image';
8005 this.upload.url = this.opts.imageUpload;
8006 this.upload.callback = this.image.insert;
8007
8008 var blob = this.utils.dataURItoBlob(e.target.result);
8009
8010 formData.append('clipboard', 1);
8011 formData.append(this.opts.imageUploadParam, blob);
8012
8013 this.upload.send(formData, e);
8014 this.rtePaste = false;
8015 },
8016 insert: function (html, data) {
8017 if (data.pre) {
8018 this.insert.raw(html);
8019 }
8020 else if (data.text) {
8021 this.insert.text(html);
8022 }
8023 else {
8024 this.insert.html(html, data);
8025 }
8026
8027 // Firefox Clipboard Observe
8028 if (this.detect.isFirefox() && this.opts.imageUpload && this.opts.clipboardImageUpload) {
8029 setTimeout($.proxy(this.paste.clipboardUpload, this), 100);
8030 }
8031
8032 }
8033 };
8034 },
8035
8036 // =placeholder -- UNSUPPORTED MODULE
8037 placeholder: function () {
8038 return {
8039 enable: function () {},
8040 show: function () {},
8041 update: function () {},
8042 hide: function () {},
8043 is: function () {},
8044 init: function () {},
8045 enabled: function () {},
8046 enableEvents: function () {},
8047 disableEvents: function () {},
8048 build: function () {},
8049 buildPosition: function () {},
8050 getPosition: function () {},
8051 isEditorEmpty: function () {},
8052 isAttr: function () {},
8053 destroy: function () {}
8054 };
8055 },
8056
8057 // =progress -- UNSUPPORTED MODULE
8058 progress: function () {
8059 return {
8060 $box: null,
8061 $bar: null,
8062 target: document.body, // or id selector
8063 show: function () {},
8064 hide: function () {},
8065 update: function () {},
8066 is: function () {},
8067 build: function () {},
8068 destroy: function () {}
8069 };
8070 },
8071
8072 // =selection
8073 selection: function () {
8074 return {
8075 get: function () {
8076 if (window.getSelection) {
8077 return window.getSelection();
8078 }
8079 else if (document.selection && document.selection.type !== 'Control') {
8080 return document.selection;
8081 }
8082
8083 return null;
8084 },
8085 range: function (sel) {
8086 if (typeof sel === 'undefined') {
8087 sel = this.selection.get();
8088 }
8089
8090 if (sel.getRangeAt && sel.rangeCount) {
8091 return sel.getRangeAt(0);
8092 }
8093
8094 return null;
8095 },
8096 is: function () {
8097 return (this.selection.isCollapsed()) ? false : true;
8098 },
8099 isRedactor: function () {
8100 var range = this.selection.range();
8101
8102 if (range !== null) {
8103 var el = range.startContainer.parentNode;
8104
8105 if ($(el).hasClass('redactor-in') || $(el).parents('.redactor-in').length !== 0) {
8106 return true;
8107 }
8108 }
8109
8110 return false;
8111 },
8112 isCollapsed: function () {
8113 var sel = this.selection.get();
8114
8115 return (sel === null) ? false : sel.isCollapsed;
8116 },
8117 update: function (sel, range) {
8118 if (range === null) {
8119 return;
8120 }
8121
8122 sel.removeAllRanges();
8123 sel.addRange(range);
8124 },
8125 current: function () {
8126 var sel = this.selection.get();
8127
8128 return (sel === null) ? false : sel.anchorNode;
8129 },
8130 parent: function () {
8131 var current = this.selection.current();
8132
8133 return (current === null) ? false : current.parentNode;
8134 },
8135 block: function (node) {
8136 node = node || this.selection.current();
8137
8138 while (node) {
8139 if (this.utils.isBlockTag(node.tagName)) {
8140 return ($(node).hasClass('redactor-in')) ? false : node;
8141 }
8142
8143 node = node.parentNode;
8144 }
8145
8146 return false;
8147 },
8148 inline: function (node) {
8149 node = node || this.selection.current();
8150
8151 while (node) {
8152 if (this.utils.isInlineTag(node.tagName)) {
8153 return ($(node).hasClass('redactor-in')) ? false : node;
8154 }
8155
8156 node = node.parentNode;
8157 }
8158
8159 return false;
8160 },
8161 element: function (node) {
8162 if (!node) {
8163 node = this.selection.current();
8164 }
8165
8166 while (node) {
8167 if (node.nodeType === 1) {
8168 if ($(node).hasClass('redactor-in')) {
8169 return false;
8170 }
8171
8172 return node;
8173 }
8174
8175 node = node.parentNode;
8176 }
8177
8178 return false;
8179 },
8180 prev: function () {
8181 var current = this.selection.current();
8182
8183 return (current === null) ? false : this.selection.current().previousSibling;
8184 },
8185 next: function () {
8186 var current = this.selection.current();
8187
8188 return (current === null) ? false : this.selection.current().nextSibling;
8189 },
8190 blocks: function (tag) {
8191 var blocks = [];
8192 var nodes = this.selection.nodes(tag);
8193
8194 $.each(nodes, $.proxy(function (i, node) {
8195 if (this.utils.isBlock(node)) {
8196 blocks.push(node);
8197 }
8198
8199 }, this));
8200
8201 var block = this.selection.block();
8202 if (blocks.length === 0 && block === false) {
8203 return [];
8204 }
8205 else if (blocks.length === 0 && block !== false) {
8206 return [block];
8207 }
8208 else {
8209 return blocks;
8210 }
8211
8212 },
8213 inlines: function (tag) {
8214 var inlines = [];
8215 var nodes = this.selection.nodes(tag);
8216
8217 $.each(nodes, $.proxy(function (i, node) {
8218 if (this.utils.isInline(node)) {
8219 inlines.push(node);
8220 }
8221
8222 }, this));
8223
8224 var inline = this.selection.inline();
8225 if (inlines.length === 0 && inline === false) {
8226 return [];
8227 }
8228 else if (inlines.length === 0 && inline !== false) {
8229 return [inline];
8230 }
8231 else {
8232 return inlines;
8233 }
8234 },
8235 nodes: function (tag) {
8236 var filter = (typeof tag === 'undefined') ? [] : (($.isArray(tag)) ? tag : [tag]);
8237
8238 var sel = this.selection.get();
8239 var range = this.selection.range(sel);
8240 var nodes = [];
8241 var resultNodes = [];
8242
8243 if (this.utils.isCollapsed()) {
8244 nodes = [this.selection.current()];
8245 }
8246 else {
8247 var node = range.startContainer;
8248 var endNode = range.endContainer;
8249
8250 // single node
8251 if (node === endNode) {
8252 return [node];
8253 }
8254
8255 // iterate
8256 while (node && node !== endNode) {
8257 nodes.push(node = this.selection.nextNode(node));
8258 }
8259
8260 // partially selected nodes
8261 node = range.startContainer;
8262 while (node && node !== range.commonAncestorContainer) {
8263 nodes.unshift(node);
8264 node = node.parentNode;
8265 }
8266 }
8267
8268 // remove service nodes
8269 $.each(nodes, function (i, s) {
8270 if (s) {
8271 var tagName = (s.nodeType !== 1) ? false : s.tagName.toLowerCase();
8272
8273 if ($(s).hasClass('redactor-script-tag') || $(s).hasClass(
8274 'redactor-selection-marker')) {
8275 return;
8276 }
8277 else if (tagName && filter.length !== 0 && $.inArray(tagName,
8278 filter
8279 ) === -1) {
8280 return;
8281 }
8282 else {
8283 resultNodes.push(s);
8284 }
8285 }
8286 });
8287
8288 return (resultNodes.length === 0) ? [] : resultNodes;
8289 },
8290 nextNode: function (node) {
8291 if (node.hasChildNodes()) {
8292 return node.firstChild;
8293 }
8294 else {
8295 while (node && !node.nextSibling) {
8296 node = node.parentNode;
8297 }
8298
8299 if (!node) {
8300 return null;
8301 }
8302
8303 return node.nextSibling;
8304 }
8305 },
8306 save: function () {
8307 this.marker.insert();
8308 this.savedSel = this.core.editor().html();
8309 },
8310 restore: function (removeMarkers) {
8311 var node1 = this.marker.find(1);
8312 var node2 = this.marker.find(2);
8313
8314 if (this.detect.isFirefox()) {
8315 this.core.editor().focus();
8316 }
8317
8318 if (node1.length !== 0 && node2.length !== 0) {
8319 this.caret.set(node1, node2);
8320 }
8321 else if (node1.length !== 0) {
8322 this.caret.start(node1);
8323 }
8324 else {
8325 this.core.editor().focus();
8326 }
8327
8328 if (removeMarkers !== false) {
8329 this.marker.remove();
8330 this.savedSel = false;
8331 }
8332 },
8333 saveInstant: function () {
8334 var el = this.core.editor()[0];
8335 var doc = el.ownerDocument, win = doc.defaultView;
8336 var sel = win.getSelection();
8337
8338 if (!sel.getRangeAt || !sel.rangeCount) {
8339 return;
8340 }
8341
8342 var range = sel.getRangeAt(0);
8343 var selectionRange = range.cloneRange();
8344
8345 selectionRange.selectNodeContents(el);
8346 selectionRange.setEnd(range.startContainer, range.startOffset);
8347
8348 var start = selectionRange.toString().length;
8349
8350 this.saved = {
8351 start: start,
8352 end: start + range.toString().length,
8353 node: range.startContainer
8354 };
8355
8356 return this.saved;
8357 },
8358 restoreInstant: function (saved) {
8359 if (typeof saved === 'undefined' && !this.saved) {
8360 return;
8361 }
8362
8363 this.saved = (typeof saved !== 'undefined') ? saved : this.saved;
8364
8365 var $node = this.core.editor().find(this.saved.node);
8366 if ($node.length !== 0 && $node.text().trim().replace(/\u200B/g,
8367 ''
8368 ).length === 0) {
8369 try {
8370 var range = document.createRange();
8371 range.setStart($node[0], 0);
8372
8373 var sel = window.getSelection();
8374 sel.removeAllRanges();
8375 sel.addRange(range);
8376 }
8377 catch (e) {}
8378
8379 return;
8380 }
8381
8382 var el = this.core.editor()[0];
8383 var doc = el.ownerDocument, win = doc.defaultView;
8384 var charIndex = 0, range = doc.createRange();
8385
8386 range.setStart(el, 0);
8387 range.collapse(true);
8388
8389 var nodeStack = [el], node, foundStart = false, stop = false;
8390 while (!stop && (node = nodeStack.pop())) {
8391 if (node.nodeType == 3) {
8392 var nextCharIndex = charIndex + node.length;
8393 if (!foundStart && this.saved.start >= charIndex && this.saved.start <= nextCharIndex) {
8394 range.setStart(node, this.saved.start - charIndex);
8395 foundStart = true;
8396 }
8397
8398 if (foundStart && this.saved.end >= charIndex && this.saved.end <= nextCharIndex) {
8399 range.setEnd(node, this.saved.end - charIndex);
8400 stop = true;
8401 }
8402 charIndex = nextCharIndex;
8403 }
8404 else {
8405 var i = node.childNodes.length;
8406 while (i--) {
8407 nodeStack.push(node.childNodes[i]);
8408 }
8409 }
8410 }
8411
8412 var sel = win.getSelection();
8413 sel.removeAllRanges();
8414 sel.addRange(range);
8415 },
8416 node: function (node) {
8417 $(node).prepend(this.marker.get(1));
8418 $(node).append(this.marker.get(2));
8419
8420 this.selection.restore();
8421 },
8422 all: function () {
8423 this.core.editor().focus();
8424
8425 var sel = this.selection.get();
8426 var range = this.selection.range(sel);
8427
8428 range.selectNodeContents(this.core.editor()[0]);
8429
8430 this.selection.update(sel, range);
8431 },
8432 remove: function () {
8433 this.selection.get().removeAllRanges();
8434 },
8435 replace: function (html) {
8436 this.insert.html(html);
8437 },
8438 text: function () {
8439 return this.selection.get().toString();
8440 },
8441 html: function () {
8442 var html = '';
8443 var sel = this.selection.get();
8444
8445 if (sel.rangeCount) {
8446 var container = document.createElement('div');
8447 var len = sel.rangeCount;
8448 for (var i = 0; i < len; ++i) {
8449 container.appendChild(sel.getRangeAt(i).cloneContents());
8450 }
8451
8452 html = this.clean.onGet(container.innerHTML);
8453 }
8454
8455 return html;
8456 },
8457 extractEndOfNode: function (node) {
8458 var sel = this.selection.get();
8459 var range = this.selection.range(sel);
8460
8461 var clonedRange = range.cloneRange();
8462 clonedRange.selectNodeContents(node);
8463 clonedRange.setStart(range.endContainer, range.endOffset);
8464
8465 return clonedRange.extractContents();
8466 },
8467
8468 // #backward
8469 removeMarkers: function () {
8470 this.marker.remove();
8471 },
8472 marker: function (num) {
8473 return this.marker.get(num);
8474 },
8475 markerHtml: function (num) {
8476 return this.marker.html(num);
8477 }
8478
8479 };
8480 },
8481
8482 // =shortcuts
8483 shortcuts: function () {
8484 return {
8485 // based on https://github.com/jeresig/jquery.hotkeys
8486 hotkeysSpecialKeys: {
8487 8: 'backspace',
8488 9: 'tab',
8489 10: 'return',
8490 13: 'return',
8491 16: 'shift',
8492 17: 'ctrl',
8493 18: 'alt',
8494 19: 'pause',
8495 20: 'capslock',
8496 27: 'esc',
8497 32: 'space',
8498 33: 'pageup',
8499 34: 'pagedown',
8500 35: 'end',
8501 36: 'home',
8502 37: 'left',
8503 38: 'up',
8504 39: 'right',
8505 40: 'down',
8506 45: 'insert',
8507 46: 'del',
8508 59: ';',
8509 61: '=',
8510 96: '0',
8511 97: '1',
8512 98: '2',
8513 99: '3',
8514 100: '4',
8515 101: '5',
8516 102: '6',
8517 103: '7',
8518 104: '8',
8519 105: '9',
8520 106: '*',
8521 107: '+',
8522 109: '-',
8523 110: '.',
8524 111: '/',
8525 112: 'f1',
8526 113: 'f2',
8527 114: 'f3',
8528 115: 'f4',
8529 116: 'f5',
8530 117: 'f6',
8531 118: 'f7',
8532 119: 'f8',
8533 120: 'f9',
8534 121: 'f10',
8535 122: 'f11',
8536 123: 'f12',
8537 144: 'numlock',
8538 145: 'scroll',
8539 173: '-',
8540 186: ';',
8541 187: '=',
8542 188: ',',
8543 189: '-',
8544 190: '.',
8545 191: '/',
8546 192: '`',
8547 219: '[',
8548 220: '\\',
8549 221: ']',
8550 222: '\''
8551 },
8552 hotkeysShiftNums: {
8553 '`': '~',
8554 '1': '!',
8555 '2': '@',
8556 '3': '#',
8557 '4': '$',
8558 '5': '%',
8559 '6': '^',
8560 '7': '&',
8561 '8': '*',
8562 '9': '(',
8563 '0': ')',
8564 '-': '_',
8565 '=': '+',
8566 ';': ': ',
8567 '\'': '"',
8568 ',': '<',
8569 '.': '>',
8570 '/': '?',
8571 '\\': '|'
8572 },
8573 init: function (e, key) {
8574 // disable browser's hot keys for bold and italic if shortcuts off
8575 if (this.opts.shortcuts === false) {
8576 if ((e.ctrlKey || e.metaKey) && (key === 66 || key === 73)) {
8577 e.preventDefault();
8578 }
8579
8580 return false;
8581 }
8582 else {
8583 // build
8584 $.each(this.opts.shortcuts, $.proxy(function (str, command) {
8585 this.shortcuts.build(e, str, command);
8586
8587 }, this));
8588 }
8589 },
8590 build: function (e, str, command) {
8591 var handler = $.proxy(function () {
8592 this.shortcuts.buildHandler(command);
8593
8594 }, this);
8595
8596 var keys = str.split(',');
8597 var len = keys.length;
8598 for (var i = 0; i < len; i++) {
8599 if (typeof keys[i] === 'string') {
8600 this.shortcuts.handler(e, $.trim(keys[i]), handler);
8601 }
8602 }
8603
8604 },
8605 buildHandler: function (command) {
8606 var func;
8607 if (command.func.search(/\./) !== '-1') {
8608 func = command.func.split('.');
8609 if (typeof this[func[0]] !== 'undefined') {
8610 this[func[0]][func[1]].apply(this, command.params);
8611 }
8612 }
8613 else {
8614 this[command.func].apply(this, command.params);
8615 }
8616 },
8617 handler: function (e, keys, origHandler) {
8618 keys = keys.toLowerCase().split(' ');
8619
8620 var special = this.shortcuts.hotkeysSpecialKeys[e.keyCode];
8621 var character = String.fromCharCode(e.which).toLowerCase();
8622 var modif = '', possible = {};
8623
8624 $.each(['alt', 'ctrl', 'meta', 'shift'], function (index, specialKey) {
8625 if (e[specialKey + 'Key'] && special !== specialKey) {
8626 modif += specialKey + '+';
8627 }
8628 });
8629
8630 if (special) {
8631 possible[modif + special] = true;
8632 }
8633
8634 if (character) {
8635 possible[modif + character] = true;
8636 possible[modif + this.shortcuts.hotkeysShiftNums[character]] = true;
8637
8638 // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
8639 if (modif === 'shift+') {
8640 possible[this.shortcuts.hotkeysShiftNums[character]] = true;
8641 }
8642 }
8643
8644 var len = keys.length;
8645 for (var i = 0; i < len; i++) {
8646 if (possible[keys[i]]) {
8647 e.preventDefault();
8648 return origHandler.apply(this, arguments);
8649 }
8650 }
8651 }
8652 };
8653 },
8654
8655 // =storage -- UNSUPPORTED MODULE
8656 storage: function () {
8657 return {
8658 data: [],
8659 add: function () {},
8660 status: function () {},
8661 observe: function () {},
8662 changes: function () {}
8663
8664 };
8665 },
8666
8667 // =toolbar
8668 toolbar: function () {
8669 return {
8670 build: function () {
8671 this.button.hideButtons();
8672 this.button.hideButtonsOnMobile();
8673
8674 this.$toolbarBox = $('<div class="redactor-toolbar-box" />');
8675 this.$toolbarBox[0].innerHTML = '<ul class="redactor-toolbar" id="redactor-toolbar-' + this.uuid + '" role="toolbar"></ul>';
8676 this.$toolbar = $(this.$toolbarBox[0].children[0]);
8677 this.$box[0].insertBefore(this.$toolbarBox[0], this.$box[0].firstChild);
8678
8679 this.button.$toolbar = this.$toolbar;
8680 this.button.setFormatting();
8681 this.button.load(this.$toolbar);
8682
8683 require(['Core'], (function(Core) {
8684 this.$toolbar[0].addEventListener('keydown', this.toolbar.keydown.bind(this, Core));
8685 }).bind(this));
8686 },
8687 createContainer: function () {},
8688 append: function () {},
8689 setOverflow: function () {},
8690 setFixed: function () {},
8691 setUnfixed: function () {},
8692 getBoxTop: function () {},
8693 observeScroll: function () {},
8694 observeScrollResize: function () {},
8695 observeScrollEnable: function () {},
8696 observeScrollDisable: function () {},
8697 setDropdownsFixed: function () {},
8698 unsetDropdownsFixed: function () {},
8699 setDropdownPosition: function () {},
8700 /**
8701 * @param {object} Core
8702 * @param {KeyboardEvent} event
8703 */
8704 keydown: function(Core, event) {
8705 var activeButton = document.activeElement;
8706 if (!activeButton.classList.contains('re-button')) {
8707 return;
8708 }
8709
8710 // Enter, Space, End, Home, ArrowLeft, ArrowRight, ArrowDown
8711 // Remarks: ArrowUp is not considered, because we do not support radio groups at the top level.
8712 var keyboardCodes = [13, 32, 35, 36, 37, 39, 40];
8713 if (keyboardCodes.indexOf(event.which) === -1) {
8714 return;
8715 }
8716
8717 // [Enter] || [Space]
8718 if (event.which === 13 || event.which === 32) {
8719 event.preventDefault();
8720
8721 require(['Core'], function(Core) {
8722 Core.triggerEvent(activeButton, 'mousedown');
8723 });
8724
8725 return;
8726 }
8727
8728 // [ArrowDown] opens drop-down menus, but does nothing on "regular" buttons.
8729 if (event.which === 40) {
8730 if (elAttr(activeButton, 'aria-haspopup') !== 'true') {
8731 return;
8732 }
8733
8734 event.preventDefault();
8735 Core.triggerEvent(activeButton, 'mousedown');
8736
8737 var dropdown = $(activeButton).data('dropdown');
8738 var firstItem = elBySel('li', dropdown[0]);
8739 if (firstItem) firstItem.focus();
8740 return;
8741 }
8742
8743 event.preventDefault();
8744
8745 var buttons = Array.prototype.slice.call(elBySelAll('.re-button', this.$toolbar[0]));
8746 var newActiveButton = null;
8747 // [End]
8748 if (event.which === 35) {
8749 newActiveButton = buttons[buttons.length - 1];
8750 }
8751 // [Home]
8752 else if (event.which === 36) {
8753 newActiveButton = buttons[0];
8754 }
8755 else {
8756 var index = buttons.indexOf(activeButton);
8757
8758 // [ArrowLeft]
8759 if (event.which === 37) {
8760 index--;
8761
8762 if (index === -1) {
8763 index = buttons.length - 1;
8764 }
8765 }
8766 // [ArrowRight]
8767 else if (event.which === 39) {
8768 index++;
8769
8770 if (index === buttons.length) {
8771 index = 0;
8772 }
8773 }
8774
8775 newActiveButton = buttons[index];
8776 }
8777
8778 if (newActiveButton !== null) {
8779 newActiveButton.focus();
8780 }
8781 }
8782 };
8783 },
8784
8785 // =upload -- UNSUPPORTED MODULE
8786 upload: function () {
8787 return {
8788 init: function () {},
8789 directUpload: function () {},
8790 onDrop: function () {},
8791 traverseFile: function () {},
8792 setConfig: function () {},
8793 getType: function () {},
8794 getHiddenFields: function () {},
8795 send: function () {},
8796 onDrag: function () {},
8797 onDragLeave: function () {},
8798 clearImageFields: function () {},
8799 addImageFields: function () {},
8800 removeImageFields: function () {},
8801 clearFileFields: function () {},
8802 addFileFields: function () {},
8803 removeFileFields: function () {}
8804 };
8805 },
8806
8807 // =s3 -- UNSUPPORTED MODULE
8808 uploads3: function () {
8809 return {
8810 send: function () {},
8811 executeOnSignedUrl: function () {},
8812 createCORSRequest: function () {},
8813 sendToS3: function () {}
8814 };
8815 },
8816
8817 // =utils
8818 utils: function () {
8819 return {
8820 isEmpty: function (html) {
8821 html = (typeof html === 'undefined') ? this.core.editor().html() : html;
8822
8823 html = html.replace(/[\u200B-\u200D\uFEFF]/g, '');
8824 html = html.replace(/&nbsp;/gi, '');
8825 html = html.replace(/<\/?br\s?\/?>/g, '');
8826 html = html.replace(/\s/g, '');
8827 html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
8828 html = html.replace(/<iframe(.*?[^>])>$/i, 'iframe');
8829 html = html.replace(/<source(.*?[^>])>$/i, 'source');
8830
8831 // remove empty tags
8832 html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8833 html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8834
8835 html = $.trim(html);
8836
8837 return html === '';
8838 },
8839 isElement: function (obj) {
8840 try {
8841 // Using W3 DOM2 (works for FF, Opera and Chrome)
8842 return obj instanceof HTMLElement;
8843 }
8844 catch (e) {
8845 return (typeof obj === 'object') && (obj.nodeType === 1) && (typeof obj.style === 'object') && (typeof obj.ownerDocument === 'object');
8846 }
8847 },
8848 strpos: function (haystack, needle, offset) {
8849 var i = haystack.indexOf(needle, offset);
8850 return i >= 0 ? i : false;
8851 },
8852 dataURItoBlob: function (dataURI) {
8853 var byteString;
8854 if (dataURI.split(',')[0].indexOf('base64') >= 0) {
8855 byteString = atob(dataURI.split(',')[1]);
8856 }
8857 else {
8858 byteString = unescape(dataURI.split(',')[1]);
8859 }
8860
8861 var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
8862
8863 var ia = new Uint8Array(byteString.length);
8864 for (var i = 0; i < byteString.length; i++) {
8865 ia[i] = byteString.charCodeAt(i);
8866 }
8867
8868 return new Blob([ia], {type: mimeString});
8869 },
8870 getOuterHtml: function (el) {
8871 return $('<div>').append($(el).eq(0).clone()).html();
8872 },
8873 cloneAttributes: function (from, to) {
8874 from = from[0] || from;
8875 to = $(to);
8876
8877 var attrs = from.attributes;
8878 var len = attrs.length;
8879 while (len--) {
8880 var attr = attrs[len];
8881 to.attr(attr.name, attr.value);
8882 }
8883
8884 return to;
8885 },
8886 breakBlockTag: function () {
8887 var block = this.selection.block();
8888 if (!block) {
8889 return false;
8890 }
8891
8892 var isEmpty = this.utils.isEmpty(block.innerHTML);
8893
8894 var tag = block.tagName.toLowerCase();
8895 if (tag === 'pre' || tag === 'li' || tag === 'td' || tag === 'th') {
8896 return false;
8897 }
8898
8899 if (!isEmpty && this.utils.isStartOfElement(block)) {
8900 return {
8901 $block: $(block),
8902 $next: $(block).next(),
8903 type: 'start'
8904 };
8905 }
8906 else if (!isEmpty && this.utils.isEndOfElement(block)) {
8907 return {
8908 $block: $(block),
8909 $next: $(block).next(),
8910 type: 'end'
8911 };
8912 }
8913 else {
8914 var endOfNode = this.selection.extractEndOfNode(block);
8915 var $nextPart = $('<' + tag + ' />').append(endOfNode);
8916
8917 $nextPart = this.utils.cloneAttributes(block, $nextPart);
8918 $(block).after($nextPart);
8919
8920 return {
8921 $block: $(block),
8922 $next: $nextPart,
8923 type: 'break'
8924 };
8925 }
8926 }, // tag detection
8927 inBlocks: function (tags) {
8928 tags = ($.isArray(tags)) ? tags : [tags];
8929
8930 var blocks = this.selection.blocks();
8931 var len = blocks.length;
8932 var contains = false;
8933 for (var i = 0; i < len; i++) {
8934 if (blocks[i] !== false) {
8935 var tag = blocks[i].tagName.toLowerCase();
8936
8937 if ($.inArray(tag, tags) !== -1) {
8938 contains = true;
8939 }
8940 }
8941 }
8942
8943 return contains;
8944
8945 },
8946 inInlines: function (tags) {
8947 tags = ($.isArray(tags)) ? tags : [tags];
8948
8949 var inlines = this.selection.inlines();
8950 var len = inlines.length;
8951 var contains = false;
8952 for (var i = 0; i < len; i++) {
8953 var tag = inlines[i].tagName.toLowerCase();
8954
8955 if ($.inArray(tag, tags) !== -1) {
8956 contains = true;
8957 }
8958 }
8959
8960 return contains;
8961
8962 },
8963 isTag: function (current, tag) {
8964 var element = $(current).closest(tag, this.core.editor()[0]);
8965 if (element.length === 1) {
8966 return element[0];
8967 }
8968
8969 return false;
8970 },
8971 isBlock: function (block) {
8972 if (block === null) {
8973 return false;
8974 }
8975
8976 block = block[0] || block;
8977
8978 return block && this.utils.isBlockTag(block.tagName);
8979 },
8980 isBlockTag: function (tag) {
8981 return (typeof tag === 'undefined') ? false : this.reIsBlock.test(tag);
8982 },
8983 isInline: function (inline) {
8984 inline = inline[0] || inline;
8985
8986 return inline && this.utils.isInlineTag(inline.tagName);
8987 },
8988 isInlineTag: function (tag) {
8989 return (typeof tag === 'undefined') ? false : this.reIsInline.test(tag);
8990 }, // parents detection
8991 isRedactorParent: function (el) {
8992 if (!el) {
8993 return false;
8994 }
8995
8996 if ($(el).parents('.redactor-in').length === 0 || $(el).hasClass('redactor-in')) {
8997 return false;
8998 }
8999
9000 return el;
9001 },
9002 isCurrentOrParentHeader: function () {
9003 return this.utils.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
9004 },
9005 isCurrentOrParent: function (tagName) {
9006 var parent = this.selection.parent();
9007 var current = this.selection.current();
9008
9009 if ($.isArray(tagName)) {
9010 var matched = 0;
9011 $.each(tagName, $.proxy(function (i, s) {
9012 if (this.utils.isCurrentOrParentOne(current, parent, s)) {
9013 matched++;
9014 }
9015 }, this));
9016
9017 return (matched === 0) ? false : true;
9018 }
9019 else {
9020 return this.utils.isCurrentOrParentOne(current, parent, tagName);
9021 }
9022 },
9023 isCurrentOrParentOne: function (current, parent, tagName) {
9024 tagName = tagName.toUpperCase();
9025
9026 return parent && parent.tagName === tagName ? parent : current && current.tagName === tagName ? current : false;
9027 },
9028 isEditorRelative: function () {
9029 var position = this.core.editor().css('position');
9030 var arr = ['absolute', 'fixed', 'relative'];
9031
9032 return ($.inArray(arr, position) !== -1);
9033 },
9034 setEditorRelative: function () {
9035 this.core.editor().addClass('redactor-relative');
9036 }, // scroll
9037 getScrollTarget: function () {
9038 var $scrollTarget = $(this.opts.scrollTarget);
9039
9040 return ($scrollTarget.length !== 0) ? $scrollTarget : $(document);
9041 },
9042 freezeScroll: function () {
9043 this.freezeScrollTop = this.utils.getScrollTarget().scrollTop();
9044 this.utils.getScrollTarget().scrollTop(this.freezeScrollTop);
9045 },
9046 unfreezeScroll: function () {
9047 if (typeof this.freezeScrollTop === 'undefined') {
9048 return;
9049 }
9050
9051 this.utils.getScrollTarget().scrollTop(this.freezeScrollTop);
9052 },
9053 saveScroll: function () {
9054 this.tmpScrollTop = this.utils.getScrollTarget().scrollTop();
9055 },
9056 restoreScroll: function () {
9057 if (typeof this.tmpScrollTop === 'undefined') {
9058 return;
9059 }
9060
9061 this.utils.getScrollTarget().scrollTop(this.tmpScrollTop);
9062 },
9063 isStartOfElement: function (element) {
9064 if (typeof element === 'undefined') {
9065 element = this.selection.block();
9066 if (!element) {
9067 return false;
9068 }
9069 }
9070
9071 return (this.offset.get(element) === 0) ? true : false;
9072 },
9073 isEndOfElement: function (element) {
9074 if (typeof element === 'undefined') {
9075 element = this.selection.block();
9076 if (!element) {
9077 return false;
9078 }
9079 }
9080
9081 var text = $.trim($(element).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
9082 ''
9083 );
9084 var offset = this.offset.get(element);
9085
9086 return (offset === text.length) ? true : false;
9087 },
9088 removeEmptyAttr: function (el, attr) {
9089 var $el = $(el);
9090 if (typeof $el.attr(attr) === 'undefined') {
9091 return true;
9092 }
9093
9094 if ($el.attr(attr) === '') {
9095 $el.removeAttr(attr);
9096 return true;
9097 }
9098
9099 return false;
9100 },
9101 replaceToTag: function (node, tag) {
9102 var replacement;
9103 $(node).replaceWith(function () {
9104 replacement = $('<' + tag + ' />').append($(this).contents());
9105
9106 for (var i = 0; i < this.attributes.length; i++) {
9107 replacement.attr(this.attributes[i].name,
9108 this.attributes[i].value
9109 );
9110 }
9111
9112 return replacement;
9113 });
9114
9115 return replacement;
9116 }, // select all
9117 isSelectAll: function () {
9118 return this.selectAll;
9119 },
9120 enableSelectAll: function () {
9121 this.selectAll = true;
9122 },
9123 disableSelectAll: function () {
9124 this.selectAll = false;
9125 },
9126 disableBodyScroll: function () {},
9127 measureScrollbar: function () {
9128 var $body = $('body');
9129 var scrollDiv = document.createElement('div');
9130 scrollDiv.className = 'redactor-scrollbar-measure';
9131
9132 $body.append(scrollDiv);
9133 var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
9134 $body[0].removeChild(scrollDiv);
9135 return scrollbarWidth;
9136 },
9137 enableBodyScroll: function () {},
9138 appendFields: function (appendFields, data) {
9139 if (!appendFields) {
9140 return data;
9141 }
9142 else if (typeof appendFields === 'object') {
9143 $.each(appendFields, function (k, v) {
9144 if (v !== null && v.toString().indexOf('#') === 0) {
9145 v = $(v).val();
9146 }
9147
9148 data.append(k, v);
9149
9150 });
9151
9152 return data;
9153 }
9154
9155 var $fields = $(appendFields);
9156 if ($fields.length === 0) {
9157 return data;
9158 }
9159 else {
9160 var str = '';
9161 $fields.each(function () {
9162 data.append($(this).attr('name'), $(this).val());
9163 });
9164
9165 return data;
9166 }
9167 },
9168 appendForms: function (appendForms, data) {
9169 if (!appendForms) {
9170 return data;
9171 }
9172
9173 var $forms = $(appendForms);
9174 if ($forms.length === 0) {
9175 return data;
9176 }
9177 else {
9178 var formData = $forms.serializeArray();
9179
9180 $.each(formData, function (z, f) {
9181 data.append(f.name, f.value);
9182 });
9183
9184 return data;
9185 }
9186 },
9187 isRgb: function (str) {
9188 return (str.search(/^rgb/i) === 0);
9189 },
9190 rgb2hex: function (rgb) {
9191 rgb = rgb.match(
9192 /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
9193
9194 return (rgb && rgb.length === 4) ? '#' + ('0' + parseInt(rgb[1],
9195 10
9196 ).toString(16)).slice(-2) + ('0' + parseInt(rgb[2],
9197 10
9198 ).toString(16)).slice(-2) + ('0' + parseInt(rgb[3],
9199 10
9200 ).toString(16)).slice(-2) : '';
9201 },
9202
9203 // #backward
9204 isCollapsed: function () {
9205 return this.selection.isCollapsed();
9206 },
9207 isMobile: function () {
9208 return this.detect.isMobile();
9209 },
9210 isDesktop: function () {
9211 return this.detect.isDesktop();
9212 },
9213 isPad: function () {
9214 return this.detect.isIpad();
9215 }
9216
9217 };
9218 },
9219
9220 // #backward
9221 browser: function () {
9222 return {
9223 webkit: function () {
9224 return this.detect.isWebkit();
9225 },
9226 ff: function () {
9227 return this.detect.isFirefox();
9228 },
9229 ie: function () {
9230 return this.detect.isIe();
9231 }
9232 };
9233 }
9234 };
9235
9236 $(window).on('load.tools.redactor', function () {
9237 $('[data-tools="redactor"]').redactor();
9238 });
9239
9240 // constructor
9241 Redactor.prototype.init.prototype = Redactor.prototype;
9242
9243 })(jQuery);
9244
9245 (function ($) {
9246 $.fn.redactorAnimation = function (animation, options, callback) {
9247 return this.each(function () {
9248 new redactorAnimation(this, animation, options, callback);
9249 });
9250 };
9251
9252 function redactorAnimation(element, animation, options, callback) {
9253 // default
9254 var opts = {
9255 duration: 0.5,
9256 iterate: 1,
9257 delay: 0,
9258 prefix: 'redactor-',
9259 timing: 'linear'
9260 };
9261
9262 this.animation = animation;
9263 this.slide = (this.animation === 'slideDown' || this.animation === 'slideUp');
9264 this.$element = $(element);
9265 this.prefixes = ['', '-moz-', '-o-animation-', '-webkit-'];
9266 this.queue = [];
9267
9268 // options or callback
9269 if (typeof options === 'function') {
9270 callback = options;
9271 this.opts = opts;
9272 }
9273 else {
9274 this.opts = $.extend(opts, options);
9275 }
9276
9277 // slide
9278 if (this.slide) {
9279 this.$element.height(this.$element.height());
9280 }
9281
9282 // init
9283 this.init(callback);
9284
9285 }
9286
9287 redactorAnimation.prototype = {
9288
9289 init: function (callback) {
9290 this.queue.push(this.animation);
9291
9292 this.clean();
9293
9294 if (this.animation === 'show') {
9295 this.opts.timing = 'linear';
9296 this.$element.removeClass('hide').show();
9297
9298 if (typeof callback === 'function') {
9299 callback(this);
9300 }
9301 }
9302 else if (this.animation === 'hide') {
9303 this.opts.timing = 'linear';
9304 this.$element.hide();
9305
9306 if (typeof callback === 'function') {
9307 callback(this);
9308 }
9309 }
9310 else {
9311 this.animate(callback);
9312 }
9313
9314 },
9315 animate: function (callback) {
9316 this.$element.addClass('redactor-animated').css('display', '').removeClass('hide');
9317 this.$element.addClass(this.opts.prefix + this.queue[0]);
9318
9319 this.set(this.opts.duration + 's', this.opts.delay + 's', this.opts.iterate, this.opts.timing);
9320
9321 var _callback = (this.queue.length > 1) ? null : callback;
9322 this.complete('AnimationEnd', $.proxy(function () {
9323 if (this.$element.hasClass(this.opts.prefix + this.queue[0])) {
9324 this.clean();
9325 this.queue.shift();
9326
9327 if (this.queue.length) {
9328 this.animate(callback);
9329 }
9330 }
9331
9332 }, this), _callback);
9333 },
9334 set: function (duration, delay, iterate, timing) {
9335 var len = this.prefixes.length;
9336
9337 while (len--) {
9338 this.$element.css(this.prefixes[len] + 'animation-duration', duration);
9339 this.$element.css(this.prefixes[len] + 'animation-delay', delay);
9340 this.$element.css(this.prefixes[len] + 'animation-iteration-count', iterate);
9341 this.$element.css(this.prefixes[len] + 'animation-timing-function', timing);
9342 }
9343
9344 },
9345 clean: function () {
9346 this.$element.removeClass('redactor-animated');
9347 this.$element.removeClass(this.opts.prefix + this.queue[0]);
9348
9349 this.set('', '', '', '');
9350
9351 },
9352 complete: function (type, make, callback) {
9353 this.$element.one(type.toLowerCase() + ' webkit' + type + ' o' + type + ' MS' + type,
9354 $.proxy(function () {
9355 if (typeof make === 'function') {
9356 make();
9357 }
9358
9359 if (typeof callback === 'function') {
9360 callback(this);
9361 }
9362
9363 // hide
9364 var effects = [
9365 'fadeOut',
9366 'slideUp',
9367 'zoomOut',
9368 'slideOutUp',
9369 'slideOutRight',
9370 'slideOutLeft'
9371 ];
9372 if ($.inArray(this.animation, effects) !== -1) {
9373 this.$element.css('display', 'none');
9374 }
9375
9376 // slide
9377 if (this.slide) {
9378 this.$element.css('height', '');
9379 }
9380
9381 }, this)
9382 );
9383
9384 }
9385 };
9386 })(jQuery);