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