4 Heavily modified version for WoltLab Suite.
6 http://imperavi.com/redactor/
8 Copyright (c) 2009-2017, Imperavi LLC.
9 License: http://imperavi.com/redactor/license/
11 Usage: $('#content').redactor();
17 if (!Function
.prototype.bind
) {
18 Function
.prototype.bind = function (scope
) {
21 return fn
.apply(scope
);
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
) {
38 $.fn
.redactor = function (options
) {
40 var args
= Array
.prototype.slice
.call(arguments
, 1);
42 if (typeof options
=== 'string') {
43 this.each(function () {
44 var instance
= $.data(this, 'redactor');
47 if (options
.search(/\./) !== '-1') {
48 func
= options
.split('.');
49 if (typeof instance
[func
[0]] !== 'undefined') {
50 func
= instance
[func
[0]][func
[1]];
54 func
= instance
[options
];
57 if (typeof instance
!== 'undefined' && $.isFunction(func
)) {
58 var methodVal
= func
.apply(instance
, args
);
59 if (methodVal
!== undefined && methodVal
!== instance
) {
64 $.error('No such method "' + options
+ '" for Redactor');
69 this.each(function () {
70 $.data(this, 'redactor', {});
71 $.data(this, 'redactor', Redactor(this, options
));
75 if (val
.length
=== 0) {
78 else if (val
.length
=== 1) {
88 function Redactor(el
, options
) {
89 return new Redactor
.prototype.init(el
, options
);
93 $.Redactor
= Redactor
;
94 $.Redactor
.VERSION
= '2.99'; // Fake version
95 $.Redactor
.modules
= [
96 'air', // Unsupported module
97 'autosave', // Unsupported module
109 'file', // Unsupported module
120 'linkify', // Unsupported module
128 'placeholder', // Unsupported module
129 'progress', // Unsupported module
132 'storage', // Unsupported module
134 'upload', // Unsupported module
135 'uploads3', // Unsupported module
138 'browser' // deprecated
141 $.Redactor
.settings
= {};
149 overrideStyles
: true,
151 scrollTarget
: document
,
161 minHeight
: false, // string
162 maxHeight
: false, // string
164 maxWidth
: false, // string
166 plugins
: false, // array
174 pastePlainText
: false,
221 preClass
: false, // string
222 preSpaces
: 4, // or false
223 tabAsSpaces
: false, // true or number of spaces
226 autosave
: false, // false or url
228 autosaveFields
: false,
231 imageUploadParam
: 'file',
232 imageUploadFields
: false,
233 imageUploadForms
: false,
238 imagePosition
: false,
239 imageResizable
: false,
240 imageFloatMargin
: '10px',
242 dragImageUpload
: true,
243 multipleImageUpload
: true,
244 clipboardImageUpload
: true,
247 fileUploadParam
: 'file',
248 fileUploadFields
: false,
249 fileUploadForms
: false,
250 dragFileUpload
: true,
258 linkValidation
: true,
259 pasteLinkTarget
: false,
261 videoContainerClass
: 'video-container',
265 toolbarFixedTarget
: document
,
266 toolbarFixedTopOffset
: 0, // pixels
267 toolbarExternal
: false, // ID selector
268 toolbarOverflow
: false,
273 formatting
: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
274 formattingAdd
: false,
276 buttons
: ['format', 'bold', 'italic', 'deleted', 'lists', 'image', 'file', 'link', 'horizontalrule'], // + 'horizontalrule', 'underline', 'ol', 'ul', 'indent', 'outdent'
277 buttonsTextLabeled
: false,
279 buttonsHideOnMobile
: [],
282 removeNewlines
: false,
283 removeComments
: true,
290 keepStyleAttr
: [], // tag name array
291 keepInlineOnEnter
: false,
295 'ctrl+shift+m, meta+shift+m': {func
: 'inline.removeFormat'},
297 func
: 'inline.format',
301 func
: 'inline.format',
305 func
: 'inline.format',
306 params
: ['superscript']
309 func
: 'inline.format',
310 params
: ['subscript']
312 'ctrl+k, meta+k': {func
: 'link.show'},
315 params
: ['orderedlist']
319 params
: ['unorderedlist']
324 activeButtons
: ['deleted', 'italic', 'bold'],
325 activeButtonsStates
: {
344 'deleted': 'Strikethrough',
345 'underline': 'Underline',
349 'underline-abbr': 'U',
351 'link-insert': 'Insert link',
352 'link-edit': 'Edit link',
353 'link-in-new-tab': 'Open link in new tab',
363 'paragraph': 'Normal text',
366 'heading1': 'Heading 1',
367 'heading2': 'Heading 2',
368 'heading3': 'Heading 3',
369 'heading4': 'Heading 4',
370 'heading5': 'Heading 5',
371 'heading6': 'Heading 6',
373 'optional': 'optional',
374 'unorderedlist': 'Unordered List',
375 'orderedlist': 'Ordered List',
376 'outdent': 'Outdent',
378 'horizontalrule': 'Line',
379 'upload-label': 'Drop file here or ',
380 'caption': 'Caption',
382 'bulletslist': 'Bullets',
383 'numberslist': 'Numbers',
385 'image-position': 'Position',
391 'accessibility-help-label': 'Rich text editor'
396 type
: 'textarea', // textarea, div, inline, pre
449 paragraphizeBlocks
: [
494 emptyHtml
: '<p>​</p>',
495 invisibleSpace
: '​',
496 emptyHtmlRendered
: $('').html('').html(),
497 imageTypes
: ['image/png', 'image/jpeg', 'image/gif'],
498 userAgent
: navigator
.userAgent
.toLowerCase(),
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
512 Redactor
.fn
= $.Redactor
.prototype = {
533 init: function (el
, options
) {
534 this.$element
= $(el
);
539 this.loadOptions(options
);
543 if (this.opts
.clickToEdit
&& !this.$element
.hasClass('redactor-click-to-edit')) {
544 return this.loadToEdit(options
);
546 else if (this.$element
.hasClass('redactor-click-to-edit')) {
547 this.$element
.removeClass('redactor-click-to-edit');
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');
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
;
558 // formatting storage
559 this.formatting
= {};
565 $.extend(this.opts
.shortcuts
, this.opts
.shortcutsAdd
);
568 this.$editor
= this.$element
;
570 // detect type of editor
574 this.core
.callback('start');
575 this.core
.callback('startToEdit');
582 detectType: function () {
583 if (this.build
.isInline() || this.opts
.inline
) {
584 this.opts
.type
= 'inline';
586 else if (this.build
.isTag('DIV')) {
587 this.opts
.type
= 'div';
589 else if (this.build
.isTag('PRE')) {
590 this.opts
.type
= 'pre';
593 loadToEdit: function (options
) {
595 this.$element
.on('click.redactor-click-to-edit', $.proxy(function () {
596 this.initToEdit(options
);
600 this.$element
.addClass('redactor-click-to-edit');
604 initToEdit: function (options
) {
605 $.extend(options
.callbacks
, {
606 startToEdit: function () {
607 this.insert
.node(this.marker
.get(), false);
609 initToEdit: function () {
610 this.selection
.restore();
611 this.clickToCancelStorage
= this.code
.get();
614 $(this.opts
.clickToCancel
).off('.redactor-click-to-edit');
615 $(this.opts
.clickToCancel
).show().on('click.redactor-click-to-edit',
616 $.proxy(function (e
) {
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();
628 this.$element
.on('click.redactor-click-to-edit',
629 $.proxy(function () {
630 this.initToEdit(options
);
634 this.$element
.addClass('redactor-click-to-edit');
640 $(this.opts
.clickToSave
).off('.redactor-click-to-edit');
641 $(this.opts
.clickToSave
).show().on('click.redactor-click-to-edit',
642 $.proxy(function (e
) {
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
);
654 this.$element
.addClass('redactor-click-to-edit');
662 this.$element
.redactor(options
);
663 this.$element
.off('.redactor-click-to-edit');
666 loadOptions: function (options
) {
670 if (typeof $.Redactor
.settings
.namespace !== 'undefined') {
671 if (this.$element
.hasClass($.Redactor
.settings
.namespace)) {
672 settings
= $.Redactor
.settings
;
676 settings
= $.Redactor
.settings
;
679 this.opts
= $.extend({}, $.Redactor
.opts
, this.$element
.data(), options
);
681 this.opts
= $.extend({}, this.opts
, settings
);
684 getModuleMethods: function (object
) {
685 return Object
.getOwnPropertyNames(object
).filter(function (property
) {
686 return typeof object
[property
] === 'function';
689 loadModules: function () {
690 var len
= $.Redactor
.modules
.length
;
691 for (var i
= 0; i
< len
; i
++) {
692 this.bindModuleMethods($.Redactor
.modules
[i
]);
695 bindModuleMethods: function (module
) {
696 if (typeof this[module
] === 'undefined') {
701 this[module
] = this[module
]();
703 var methods
= this.getModuleMethods(this[module
]);
704 var len
= methods
.length
;
707 for (var z
= 0; z
< len
; z
++) {
708 this[module
][methods
[z
]] = this[module
][methods
[z
]].bind(this);
712 // =air -- UNSUPPORTED MODULE
716 collapsed: function () {},
717 collapsedEnd: function () {},
718 build: function () {},
719 append: function () {},
720 createContainer: function () {},
721 show: function () {},
722 bindHide: function () {},
727 // =autosave -- UNSUPPORTED MODULE
728 autosave: function () {
732 init: function () {},
734 send: function () {},
735 getHiddenFields: function () {},
736 success: function () {},
737 disable: function () {}
744 format: function (tag
, attr
, value
, type
) {
745 tag
= (tag
=== 'quote') ? 'blockquote' : tag
;
760 if ($.inArray(tag
, this.block
.tags
) === -1) {
764 if (tag
=== 'p' && typeof attr
=== 'undefined') {
771 return (this.utils
.isCollapsed()) ? this.block
.formatCollapsed(tag
,
775 ) : this.block
.formatUncollapsed(
782 formatCollapsed: function (tag
, attr
, value
, type
) {
783 this.selection
.save();
785 var block
= this.selection
.block();
786 var currentTag
= block
.tagName
.toLowerCase();
787 if ($.inArray(currentTag
, this.block
.tags
) === -1) {
788 this.selection
.restore();
792 var clearAllAttrs
= false;
793 if (currentTag
=== tag
&& attr
=== undefined) {
795 clearAllAttrs
= true;
799 this.block
.removeAllClass();
800 this.block
.removeAllAttr();
804 if (currentTag
=== 'blockquote' && this.utils
.isEndOfElement(block
)) {
805 this.marker
.remove();
807 replaced
= document
.createElement('p');
808 replaced
.innerHTML
= this.opts
.invisibleSpace
;
810 $(block
).after(replaced
);
811 this.caret
.start(replaced
);
812 var $last
= $(block
).children().last();
814 if ($last
.length
!== 0 && $last
[0].tagName
=== 'BR') {
819 replaced
= this.utils
.replaceToTag(block
, tag
);
822 if (typeof attr
=== 'object') {
824 for (var key
in attr
) {
825 replaced
= this.block
.setAttr(replaced
, key
, attr
[key
], type
);
829 replaced
= this.block
.setAttr(replaced
, attr
, value
, type
);
833 if (tag
=== 'pre' && replaced
.length
=== 1) {
834 $(replaced
).html($.trim($(replaced
).html()));
837 this.selection
.restore();
838 this.block
.removeInlineTags(replaced
);
842 formatUncollapsed: function (tag
, attr
, value
, type
) {
843 this.selection
.save();
846 var blocks
= this.selection
.blocks();
848 if (blocks
[0] && ($(blocks
[0]).hasClass('redactor-in') || $(blocks
[0]).hasClass(
850 blocks
= this.core
.editor().find(this.opts
.blockTags
.join(', '));
853 var len
= blocks
.length
;
854 for (var i
= 0; i
< len
; i
++) {
855 var currentTag
= blocks
[i
].tagName
.toLowerCase();
856 if ($.inArray(currentTag
,
858 ) !== -1 && currentTag
!== 'figure') {
859 var block
= this.utils
.replaceToTag(blocks
[i
], tag
);
861 if (typeof attr
=== 'object') {
863 for (var key
in attr
) {
864 block
= this.block
.setAttr(block
,
872 block
= this.block
.setAttr(block
, attr
, value
, type
);
875 replaced
.push(block
);
876 this.block
.removeInlineTags(block
);
880 this.selection
.restore();
883 if (tag
=== 'pre' && replaced
.length
!== 0) {
884 var first
= replaced
[0];
885 $.each(replaced
, function (i
, s
) {
887 $(first
).append('\n' + $.trim(s
.html()));
893 replaced
.push(first
);
898 removeInlineTags: function (node
) {
899 node
= node
[0] || node
;
901 var tags
= this.opts
.inlineTags
;
902 var blocks
= ['PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
904 if ($.inArray(node
.tagName
, blocks
) === -1) {
908 if (node
.tagName
!== 'PRE') {
909 var index
= tags
.indexOf('a');
910 tags
.splice(index
, 1);
913 $(node
).find(tags
.join(',')).not('.redactor-selection-marker').contents().unwrap();
915 setAttr: function (block
, attr
, value
, type
) {
916 if (typeof attr
=== 'undefined') {
920 var func
= (typeof type
=== 'undefined') ? 'replace' : type
;
922 if (attr
=== 'class') {
923 block
= this.block
[func
+ 'Class'](value
, block
);
926 if (func
=== 'remove') {
927 block
= this.block
[func
+ 'Attr'](attr
, block
);
929 else if (func
=== 'removeAll') {
930 block
= this.block
[func
+ 'Attr'](attr
, block
);
933 block
= this.block
[func
+ 'Attr'](attr
, value
, block
);
940 getBlocks: function (block
) {
941 block
= (typeof block
=== 'undefined') ? this.selection
.blocks() : block
;
943 if ($(block
).hasClass('redactor-box')) {
945 var nodes
= this.core
.editor().children();
946 $.each(nodes
, $.proxy(function (i
, node
) {
947 if (this.utils
.isBlock(node
)) {
958 replaceClass: function (value
, block
) {
959 return $(this.block
.getBlocks(block
)).removeAttr('class').addClass(value
)[0];
961 toggleClass: function (value
, block
) {
962 return $(this.block
.getBlocks(block
)).toggleClass(value
)[0];
964 addClass: function (value
, block
) {
965 return $(this.block
.getBlocks(block
)).addClass(value
)[0];
967 removeClass: function (value
, block
) {
968 return $(this.block
.getBlocks(block
)).removeClass(value
)[0];
970 removeAllClass: function (block
) {
971 return $(this.block
.getBlocks(block
)).removeAttr('class')[0];
973 replaceAttr: function (attr
, value
, block
) {
974 block
= this.block
.removeAttr(attr
, block
);
976 return $(block
).attr(attr
, value
)[0];
978 toggleAttr: function (attr
, value
, block
) {
979 block
= this.block
.getBlocks(block
);
983 $.each(block
, function (i
, s
) {
985 if ($el
.attr(attr
)) {
986 returned
.push(self
.block
.removeAttr(attr
, s
));
989 returned
.push(self
.block
.addAttr(attr
, value
, s
));
996 addAttr: function (attr
, value
, block
) {
997 return $(this.block
.getBlocks(block
)).attr(attr
, value
)[0];
999 removeAttr: function (attr
, block
) {
1000 return $(this.block
.getBlocks(block
)).removeAttr(attr
)[0];
1002 removeAllAttr: function (block
) {
1003 block
= this.block
.getBlocks(block
);
1006 $.each(block
, function (i
, s
) {
1007 if (typeof s
.attributes
!== 'undefined') {
1008 while (s
.attributes
.length
) {
1009 s
.removeAttribute(s
.attributes
[0].name
);
1022 buffer: function () {
1024 set: function (type
) {
1025 if (typeof type
=== 'undefined') {
1026 this.buffer
.clear();
1029 if (typeof type
=== 'undefined' || type
=== 'undo') {
1030 this.buffer
.setUndo();
1033 this.buffer
.setRedo();
1036 setUndo: function () {
1037 var saved
= this.selection
.saveInstant();
1039 var last
= this.sBuffer
[this.sBuffer
.length
- 1];
1040 var current
= this.core
.editor().html();
1042 var save
= (typeof last
!== 'undefined' && (last
[0] === current
)) ? false : true;
1044 this.sBuffer
.push([current
, saved
]);
1047 //this.selection.restore();
1049 setRedo: function () {
1050 var saved
= this.selection
.saveInstant();
1051 this.sRebuffer
.push([this.core
.editor().html(), saved
]);
1052 //this.selection.restore();
1055 this.sBuffer
.push([this.core
.editor().html(), 0]);
1058 if (this.sBuffer
.length
=== 0) {
1062 var buffer
= this.sBuffer
.pop();
1064 this.buffer
.set('redo');
1065 this.core
.editor().html(buffer
[0]);
1067 this.selection
.restoreInstant(buffer
[1]);
1068 this.selection
.restore();
1069 this.observe
.load();
1072 if (this.sRebuffer
.length
=== 0) {
1076 var buffer
= this.sRebuffer
.pop();
1078 this.buffer
.set('undo');
1079 this.core
.editor().html(buffer
[0]);
1081 this.selection
.restoreInstant(buffer
[1]);
1082 this.selection
.restore();
1083 this.observe
.load();
1085 clear: function () {
1086 this.sRebuffer
= [];
1092 build: function () {
1094 start: function () {
1095 if (this.opts
.type
!== 'textarea') {
1096 throw new Error('Only `<textarea>` types are allowed.');
1099 this.build
.startTextarea();
1108 this.build
.enableEditor();
1111 this.build
.setOptions();
1114 this.build
.callEditor();
1117 createContainerBox: function () {
1118 this.$box
= $('<div class="redactor-box" role="application" />');
1120 setIn: function () {
1121 this.core
.editor().addClass('redactor-in');
1123 setId: function () {
1124 var id
= (this.opts
.type
=== 'textarea') ? 'redactor-uuid-' + this.uuid
: this.$element
.attr(
1127 this.core
.editor().attr('id',
1128 (typeof id
=== 'undefined') ? 'redactor-uuid-' + this.uuid
: id
1131 getName: function () {
1132 var name
= this.$element
.attr('name');
1134 return (typeof name
=== 'undefined') ? 'content-' + this.uuid
: name
;
1136 buildTextarea: function () {},
1137 loadFromTextarea: function () {
1138 this.$editor
= $('<div />');
1141 this.$textarea
= this.$element
;
1142 this.$element
.attr('name', this.build
.getName());
1145 this.$box
.insertAfter(this.$element
).append(this.$editor
).append(this.$element
);
1147 this.build
.setStartAttrs();
1150 this.$editor
.addClass('redactor-layer');
1151 if (this.opts
.overrideStyles
) this.$editor
.addClass('redactor-styles');
1153 this.$element
.hide();
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>');
1159 setStartAttrs: function () {
1161 'aria-labelledby': 'redactor-voice-' + this.uuid
,
1162 'role': 'presentation'
1165 startTextarea: function () {
1166 this.build
.createContainerBox();
1169 this.build
.loadFromTextarea();
1172 this.code
.start(this.core
.textarea().val());
1175 this.core
.textarea().val(this.clean
.onSync(this.$editor
.html()));
1177 isTag: function (tag
) {
1178 return (this.$element
[0].tagName
=== tag
);
1180 isInline: function () {
1181 return (!this.build
.isTag('TEXTAREA') && !this.build
.isTag('DIV') && !this.build
.isTag(
1184 enableEditor: function () {
1185 this.core
.editor().attr({'contenteditable': true});
1187 setOptions: function () {
1189 if (this.opts
.type
=== 'inline') {
1190 this.opts
.enterKey
= false;
1194 if (this.opts
.type
=== 'inline' || this.opts
.type
=== 'pre') {
1195 this.opts
.toolbarMobile
= false;
1196 this.opts
.toolbar
= false;
1201 this.core
.editor().attr('spellcheck', this.opts
.spellcheck
);
1204 if (this.opts
.structure
) {
1205 this.core
.editor().addClass('redactor-structure');
1209 if (this.opts
.stylesClass
) {
1210 this.core
.editor().addClass(this.opts
.stylesClass
);
1213 // options sets only in textarea mode
1214 if (this.opts
.type
!== 'textarea') {
1219 this.core
.box().attr('dir', this.opts
.direction
);
1220 this.core
.editor().attr('dir', this.opts
.direction
);
1223 if (this.opts
.tabindex
) {
1224 this.core
.editor().attr('tabindex', this.opts
.tabindex
);
1228 if (this.opts
.minHeight
) {
1229 this.core
.editor().css('min-height', this.opts
.minHeight
);
1232 this.core
.editor().css('min-height', '40px');
1236 if (this.opts
.maxHeight
) {
1237 this.core
.editor().css('max-height', this.opts
.maxHeight
);
1241 if (this.opts
.maxWidth
) {
1242 this.core
.editor().css({
1243 'max-width': this.opts
.maxWidth
,
1249 callEditor: function () {
1250 this.build
.disableBrowsersEditing();
1253 this.build
.setHelpers();
1256 this.toolbarsButtons
= this.button
.init();
1259 this.toolbar
.build();
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)
1266 this.core
.element().on('blur.callback.redactor', $.proxy(function () {
1267 this.button
.setInactiveAll();
1271 // modal templates init
1272 this.modal
.templates();
1275 this.build
.plugins();
1278 this.code
.html
= this.code
.cleaned(this.core
.editor().html());
1281 this.core
.callback('init');
1282 this.core
.callback('initToEdit');
1288 setHelpers: function () {
1290 if (this.opts
.focus
) {
1291 setTimeout(this.focus
.start
, 100);
1293 else if (this.opts
.focusEnd
) {
1294 setTimeout(this.focus
.end
, 100);
1298 disableBrowsersEditing: function () {
1301 document
.execCommand('enableObjectResizing', false, false);
1302 document
.execCommand('enableInlineTableEditing', false, false);
1303 // IE prevent converting links
1304 document
.execCommand('AutoUrlDetect', false, false);
1308 plugins: function () {
1309 if (!this.opts
.plugins
) {
1313 $.each(this.opts
.plugins
, $.proxy(function (i
, s
) {
1314 var func
= (typeof RedactorPlugins
!== 'undefined' && typeof RedactorPlugins
[s
] !== 'undefined') ? RedactorPlugins
: Redactor
.fn
;
1316 if (!$.isFunction(func
[s
])) {
1320 this[s
] = func
[s
]();
1323 var methods
= this.getModuleMethods(this[s
]);
1324 var len
= methods
.length
;
1327 for (var z
= 0; z
< len
; z
++) {
1328 this[s
][methods
[z
]] = this[s
][methods
[z
]].bind(this);
1332 if (typeof this[s
].langs
!== 'undefined') {
1334 if (typeof this[s
].langs
[this.opts
.lang
] !== 'undefined') {
1335 lang
= this[s
].langs
[this.opts
.lang
];
1337 else if (typeof this[s
].langs
[this.opts
.lang
] === 'undefined' && typeof this[s
].langs
.en
!== 'undefined') {
1338 lang
= this[s
].langs
.en
;
1343 $.each(lang
, function (i
, s
) {
1344 if (typeof self
.opts
.curLang
[i
] === 'undefined') {
1345 self
.opts
.curLang
[i
] = s
;
1351 if (typeof this[s
].init
=== 'function') {
1361 button: function () {
1363 toolbar: function () {
1364 return (typeof this.button
.$toolbar
=== 'undefined' || !this.button
.$toolbar
) ? this.$toolbar
: this.button
.$toolbar
;
1369 title
: this.lang
.get('format'),
1373 title
: this.lang
.get('paragraph'),
1374 func
: 'block.format'
1377 title
: this.lang
.get('quote'),
1378 func
: 'block.format'
1381 title
: this.lang
.get('code'),
1382 func
: 'block.format'
1385 title
: this.lang
.get('heading1'),
1386 func
: 'block.format'
1389 title
: this.lang
.get('heading2'),
1390 func
: 'block.format'
1393 title
: this.lang
.get('heading3'),
1394 func
: 'block.format'
1397 title
: this.lang
.get('heading4'),
1398 func
: 'block.format'
1401 title
: this.lang
.get('heading5'),
1402 func
: 'block.format'
1405 title
: this.lang
.get('heading6'),
1406 func
: 'block.format'
1411 title
: this.lang
.get('bold-abbr'),
1413 label
: this.lang
.get('bold'),
1414 func
: 'inline.format'
1417 title
: this.lang
.get('italic-abbr'),
1419 label
: this.lang
.get('italic'),
1420 func
: 'inline.format'
1423 title
: this.lang
.get('deleted-abbr'),
1425 label
: this.lang
.get('deleted'),
1426 func
: 'inline.format'
1429 title
: this.lang
.get('underline-abbr'),
1431 label
: this.lang
.get('underline'),
1432 func
: 'inline.format'
1435 title
: this.lang
.get('lists'),
1439 title
: '• ' + this.lang
.get('unorderedlist'),
1443 title
: '1. ' + this.lang
.get('orderedlist'),
1447 title
: '< ' + this.lang
.get('outdent'),
1448 func
: 'indent.decrease',
1453 'class': 'redactor-dropdown-link-inactive',
1454 'aria-disabled': true
1460 title
: '> ' + this.lang
.get('indent'),
1461 func
: 'indent.increase',
1466 'class': 'redactor-dropdown-link-inactive',
1467 'aria-disabled': true
1475 title
: '• ' + this.lang
.get('bulletslist'),
1480 title
: '1. ' + this.lang
.get('numberslist'),
1485 title
: this.lang
.get('outdent'),
1487 func
: 'indent.decrease'
1490 title
: this.lang
.get('indent'),
1492 func
: 'indent.increase'
1495 title
: this.lang
.get('image'),
1500 title
: this.lang
.get('file'),
1505 title
: this.lang
.get('link'),
1509 title
: this.lang
.get('link-insert'),
1514 title
: this.lang
.get('link-edit')
1517 title
: this.lang
.get(
1523 title
: this.lang
.get('unlink'),
1524 func
: 'link.unlink',
1529 'class': 'redactor-dropdown-link-inactive',
1530 'aria-disabled': true
1538 title
: this.lang
.get('horizontalrule'),
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
];
1552 hideButtons: function () {
1553 if (this.opts
.buttonsHide
.length
!== 0) {
1554 this.button
.hideButtonsSlicer(this.opts
.buttonsHide
);
1557 hideButtonsOnMobile: function () {
1558 if (this.detect
.isMobile() && this.opts
.buttonsHideOnMobile
.length
!== 0) {
1559 this.button
.hideButtonsSlicer(this.opts
.buttonsHideOnMobile
);
1562 hideButtonsSlicer: function (buttons
) {
1563 $.each(buttons
, $.proxy(function (i
, s
) {
1564 var index
= this.opts
.buttons
.indexOf(s
);
1566 this.opts
.buttons
.splice(index
, 1);
1571 load: function ($toolbar
) {
1572 this.button
.buttons
= [];
1574 this.opts
.buttons
.forEach((function(btnName
) {
1575 if (btnName
=== 'image' && !this.image
.is()) {
1579 if (this.toolbarsButtons
.hasOwnProperty(btnName
)) {
1580 var listItem
= elCreate('li');
1581 listItem
.appendChild(this.button
.build(btnName
,
1582 this.toolbarsButtons
[btnName
]
1584 $toolbar
[0].appendChild(listItem
);
1588 buildButtonTooltip: function () {},
1589 build: function (btnName
, btnObject
) {
1590 var $button
= $('<a href="javascript:void(null);" rel="' + btnName
+ '" />');
1592 $button
.addClass('re-button re-' + btnName
);
1598 $button
.html(btnObject
.title
);
1601 if (btnObject
.func
|| btnObject
.command
|| btnObject
.dropdown
) {
1602 this.button
.setEvent($button
, btnName
, btnObject
);
1606 if (btnObject
.dropdown
) {
1607 $button
.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup',
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
);
1615 this.button
.setupDropdown($button
[0], $dropdown
[0]);
1618 this.button
.buttons
.push($button
);
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();
1631 elData(button
, 'a11y-mouse-event', 'mousedown');
1632 elData(button
, 'aria-expanded', false);
1634 button
.addEventListener('click', function (event
) {
1635 event
.preventDefault();
1636 event
.stopPropagation();
1640 getButtons: function () {
1641 return this.button
.toolbar().find('a.re-button');
1643 getButtonsKeys: function () {
1644 return this.button
.buttons
;
1646 setEvent: function ($button
, btnName
, btnObject
) {
1647 $button
.on('mousedown', $.proxy(function (e
) {
1650 if ($button
.hasClass('redactor-button-disabled')) {
1655 var callback
= btnObject
.func
;
1657 if (btnObject
.command
) {
1659 callback
= btnObject
.command
;
1661 else if (btnObject
.dropdown
) {
1666 this.button
.toggle(e
, btnName
, type
, callback
);
1672 toggle: function (e
, btnName
, type
, callback
, args
) {
1674 if (this.detect
.isIe() || !this.detect
.isDesktop()) {
1675 this.utils
.freezeScroll();
1676 e
.returnValue
= false;
1679 if (type
=== 'command') {
1680 this.inline
.format(callback
);
1682 else if (type
=== 'dropdown') {
1683 this.dropdown
.show(e
, btnName
);
1686 this.button
.clickCallback(e
, callback
, btnName
, args
);
1689 if (type
!== 'dropdown') {
1690 this.dropdown
.hideAll(false);
1693 if (this.detect
.isIe() || !this.detect
.isDesktop()) {
1694 this.utils
.unfreezeScroll();
1697 clickCallback: function (e
, callback
, btnName
, args
) {
1700 if (e
instanceof Event
) {
1703 else if (e
&& e
.originalEvent
) {
1704 e
.originalEvent
.preventDefault();
1707 args
= (typeof args
=== 'undefined') ? btnName
: args
;
1709 if ($.isFunction(callback
)) {
1710 callback
.call(this, btnName
);
1712 else if (callback
.search(/\./) !== '-1') {
1713 func
= callback
.split('.');
1714 if (typeof this[func
[0]] === 'undefined') {
1718 if (typeof args
=== 'object') {
1719 this[func
[0]][func
[1]].apply(this, args
);
1722 this[func
[0]][func
[1]].call(this, args
);
1727 if (typeof args
=== 'object') {
1728 this[callback
].apply(this, args
);
1731 this[callback
].call(this, args
);
1735 this.observe
.buttons(e
, btnName
);
1739 return this.button
.buttons
;
1741 get: function (key
) {
1742 if (this.opts
.toolbar
=== false) {
1746 return this.button
.toolbar().find('a.re-' + key
);
1748 set: function (key
, title
) {
1749 if (this.opts
.toolbar
=== false) {
1753 var $btn
= this.button
.toolbar().find('a.re-' + key
);
1755 $btn
.html(title
).attr('aria-label', title
);
1759 add: function (key
, title
) {
1760 if (this.button
.isAdded(key
) !== true) {
1764 var btn
= this.button
.build(key
, {title
: title
});
1766 this.button
.toolbar().append($('<li>').append(btn
));
1770 addFirst: function (key
, title
) {
1771 if (this.button
.isAdded(key
) !== true) {
1775 var btn
= this.button
.build(key
, {title
: title
});
1777 this.button
.toolbar().prepend($('<li>').append(btn
));
1781 addAfter: function (afterkey
, key
, title
) {
1782 if (this.button
.isAdded(key
) !== true) {
1786 var btn
= this.button
.build(key
, {title
: title
});
1787 var $btn
= this.button
.get(afterkey
);
1789 if ($btn
.length
!== 0) {
1790 $btn
.parent().after($('<li>').append(btn
));
1793 this.button
.toolbar().append($('<li>').append(btn
));
1798 addBefore: function (beforekey
, key
, title
) {
1799 if (this.button
.isAdded(key
) !== true) {
1803 var btn
= this.button
.build(key
, {title
: title
});
1804 var $btn
= this.button
.get(beforekey
);
1806 if ($btn
.length
!== 0) {
1807 $btn
.parent().before($('<li>').append(btn
));
1810 this.button
.toolbar().append($('<li>').append(btn
));
1815 isAdded: function (key
) {
1816 var index
= this.opts
.buttonsHideOnMobile
.indexOf(key
);
1817 if (this.opts
.toolbar
=== false || (index
!== -1 && this.detect
.isMobile())) {
1823 setIcon: function ($btn
, icon
) {
1824 if (!this.opts
.buttonsTextLabeled
) {
1825 $btn
.html(icon
).addClass('re-button-icon');
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
);
1834 addCallback: function ($btn
, callback
) {
1835 if (typeof $btn
=== 'undefined' || this.opts
.toolbar
=== false) {
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')) {
1846 this.button
.toggle(e
, key
, type
, callback
);
1850 addDropdown: function ($btn
, dropdown
) {
1851 if (this.opts
.toolbar
=== false) {
1855 $btn
.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true);
1857 var key
= $btn
.attr('rel');
1858 this.button
.addCallback($btn
, 'dropdown');
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
);
1865 this.dropdown
.build(key
, $dropdown
, dropdown
);
1867 this.button
.setupDropdown($btn
[0], $dropdown
[0]);
1872 setActive: function (key
) {
1873 this.button
.get(key
).addClass('redactor-act').attr({
1874 'aria-pressed': true,
1878 setInactive: function (key
) {
1879 this.button
.get(key
).removeClass('redactor-act').attr({
1880 'aria-pressed': false,
1881 tabindex
: (key
=== 'html') ? 0 : -1
1884 setInactiveAll: function (key
) {
1885 var $btns
= this.button
.toolbar().find('a.re-button');
1887 if (typeof key
!== 'undefined') {
1888 $btns
= $btns
.not('.re-' + key
);
1891 $btns
.removeClass('redactor-act').attr({
1892 'aria-pressed': false,
1893 tabindex
: (key
=== 'html') ? 0 : -1
1896 disable: function (key
) {
1897 this.button
.get(key
).addClass('redactor-button-disabled').attr('aria-disabled', true);
1899 enable: function (key
) {
1900 this.button
.get(key
).removeClass('redactor-button-disabled').attr('aria-disabled', false);
1902 disableAll: function (key
) {
1903 var $btns
= this.button
.toolbar().find('a.re-button');
1904 if (typeof key
!== 'undefined') {
1905 if (!Array
.isArray(key
)) {
1909 key
= key
.map(function(value
) {
1910 return '.re-' + value
1913 $btns
= $btns
.not(key
.join(','));
1916 $btns
.addClass('redactor-button-disabled').attr('aria-disabled', true);
1918 enableAll: function () {
1919 this.button
.toolbar().find('a.re-button').removeClass('redactor-button-disabled').attr('aria-disabled', false);
1921 remove: function (key
) {
1922 this.button
.get(key
).remove();
1928 caret: function () {
1930 set: function (node1
, node2
, end
) {
1931 var cs
= this.core
.editor().scrollTop();
1932 this.core
.editor().focus();
1933 this.core
.editor().scrollTop(cs
);
1935 end
= (typeof end
=== 'undefined') ? 0 : 1;
1937 node1
= node1
[0] || node1
;
1938 node2
= node2
[0] || node2
;
1940 var sel
= this.selection
.get();
1941 var range
= this.selection
.range(sel
);
1944 range
.setStart(node1
, 0);
1945 range
.setEnd(node2
, end
);
1949 this.selection
.update(sel
, range
);
1951 prepare: function (node
) {
1953 if (this.detect
.isFirefox() && typeof this.start
!== 'undefined') {
1954 this.core
.editor().focus();
1957 return node
[0] || node
;
1959 start: function (node
) {
1961 node
= this.caret
.prepare(node
);
1967 if (node
.tagName
=== 'BR') {
1968 return this.caret
.before(node
);
1971 var $first
= $(node
).children().first();
1973 // empty or inline tag
1974 var inline
= this.utils
.isInlineTag(node
.tagName
);
1975 if (node
.innerHTML
=== '' || inline
) {
1976 this.caret
.setStartEmptyOrInline(node
, inline
);
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);
1984 sel
= window
.getSelection();
1985 sel
.removeAllRanges();
1987 range
= document
.createRange();
1988 range
.selectNodeContents(node
);
1989 range
.collapse(true);
1990 sel
.addRange(range
);
1994 setStartEmptyOrInline: function (node
, inline
) {
1995 var sel
= window
.getSelection();
1996 var range
= document
.createRange();
1997 var textNode
= document
.createTextNode('\u200B');
1999 range
.setStart(node
, 0);
2000 range
.insertNode(textNode
);
2001 range
.setStartAfter(textNode
);
2002 range
.collapse(true);
2004 sel
.removeAllRanges();
2005 sel
.addRange(range
);
2007 // remove invisible text node
2009 this.core
.editor().on('keydown.redactor-remove-textnode', function () {
2010 $(textNode
).remove();
2011 $(this).off('keydown.redactor-remove-textnode');
2015 end: function (node
) {
2017 node
= this.caret
.prepare(node
);
2024 if (node
.tagName
!== 'BR' && node
.innerHTML
=== '') {
2025 return this.caret
.start(node
);
2029 if (node
.tagName
=== 'BR') {
2030 var space
= document
.createElement('span');
2031 space
.className
= 'redactor-invisible-space';
2032 space
.innerHTML
= '​';
2034 $(node
).after(space
);
2036 sel
= window
.getSelection();
2037 sel
.removeAllRanges();
2039 range
= document
.createRange();
2041 range
.setStartBefore(space
);
2042 range
.setEndBefore(space
);
2043 sel
.addRange(range
);
2045 $(space
).replaceWith(function () {
2046 return $(this).contents();
2052 if (node
.lastChild
&& node
.lastChild
.nodeType
=== 1) {
2053 return this.caret
.after(node
.lastChild
);
2056 var sel
= window
.getSelection();
2057 if (sel
.getRangeAt
|| sel
.rangeCount
) {
2059 var range
= sel
.getRangeAt(0);
2060 range
.selectNodeContents(node
);
2061 range
.collapse(false);
2063 sel
.removeAllRanges();
2064 sel
.addRange(range
);
2069 after: function (node
) {
2071 node
= this.caret
.prepare(node
);
2077 if (node
.tagName
=== 'BR') {
2078 return this.caret
.end(node
);
2082 if (this.utils
.isBlockTag(node
.tagName
)) {
2083 var next
= this.caret
.next(node
);
2085 if (typeof next
=== 'undefined') {
2086 this.caret
.end(node
);
2090 if (next
.tagName
=== 'TABLE') {
2091 next
= $(next
).find('th, td').first()[0];
2094 else if (next
.tagName
=== 'UL' || next
.tagName
=== 'OL') {
2095 next
= $(next
).find('li').first()[0];
2098 this.caret
.start(next
);
2105 var textNode
= document
.createTextNode('\u200B');
2107 sel
= window
.getSelection();
2108 sel
.removeAllRanges();
2110 range
= document
.createRange();
2111 range
.setStartAfter(node
);
2112 range
.insertNode(textNode
);
2113 range
.setStartAfter(textNode
);
2114 range
.collapse(true);
2116 sel
.addRange(range
);
2119 before: function (node
) {
2121 node
= this.caret
.prepare(node
);
2128 if (this.utils
.isBlockTag(node
.tagName
)) {
2129 var prev
= this.caret
.prev(node
);
2131 if (typeof prev
=== 'undefined') {
2132 this.caret
.start(node
);
2136 if (prev
.tagName
=== 'TABLE') {
2137 prev
= $(prev
).find('th, td').last()[0];
2140 else if (prev
.tagName
=== 'UL' || prev
.tagName
=== 'OL') {
2141 prev
= $(prev
).find('li').last()[0];
2144 this.caret
.end(prev
);
2151 sel
= window
.getSelection();
2152 sel
.removeAllRanges();
2154 range
= document
.createRange();
2156 range
.setStartBefore(node
);
2157 range
.collapse(true);
2159 sel
.addRange(range
);
2161 next: function (node
) {
2162 var $next
= $(node
).next();
2163 if ($next
.hasClass('redactor-script-tag, redactor-selection-marker')) {
2164 return $next
.next()[0];
2170 prev: function (node
) {
2171 var $prev
= $(node
).prev();
2172 if ($prev
.hasClass('redactor-script-tag, redactor-selection-marker')) {
2173 return $prev
.prev()[0];
2181 offset: function (node
) {
2182 return this.offset
.get(node
);
2189 clean: function () {
2191 onSet: function (html
) {
2192 html
= this.clean
.savePreCode(html
);
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>'
2202 // converting entity
2203 html
= html
.replace(/\$/g, '$');
2204 html
= html
.replace(/&/g, '&');
2206 // replace special characters in links
2207 html
= html
.replace(/<a href="(.*?[^>]?)®(.*?[^>]?)">/gi,
2208 '<a href="$1®$2">'
2212 html
= html
.replace(/<span id="selection-marker-1"(.*?[^>]?)><\/span>/gi,
2215 html
= html
.replace(/<span id="selection-marker-2"(.*?[^>]?)><\/span>/gi,
2221 var $div
= $('<div/>').html($.parseHTML(html
, document
, true));
2223 var replacement
= this.opts
.replaceTags
;
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()]
2234 $div
.find('span, a').attr('data-redactor-span', true);
2237 $div
.find(this.opts
.inlineTags
.join(',')).each(function () {
2241 if ($el
.attr('style')) {
2242 $el
.attr('data-redactor-style-cache', $el
.attr('style'));
2249 var tags
= ['font', 'html', 'head', 'link', 'body', 'meta', 'applet'];
2250 if (!this.opts
.script
) {
2251 tags
.push('script');
2254 html
= this.clean
.stripTags(html
, tags
);
2256 // remove html comments
2257 if (this.opts
.removeComments
) {
2258 html
= html
.replace(/<!--[\s\S]*?-->/gi, '');
2262 html
= this.paragraphize
.load(html
);
2265 html
= html
.replace(
2267 '<span id="selection-marker-1" class="redactor-selection-marker"></span>'
2269 html
= html
.replace(
2271 '<span id="selection-marker-2" class="redactor-selection-marker"></span>'
2275 if (html
.search(/^(||\s||<br\s?\/?>|| )$/i) !== -1) {
2276 return this.opts
.emptyHtml
;
2281 onGet: function (html
) {
2282 return this.clean
.onSync(html
);
2284 onSync: function (html
) {
2285 // remove invisible spaces
2286 html
= html
.replace(/\u200B/g, '');
2287 html
= html
.replace(/​/gi, '');
2288 //html = html.replace(/ /gi, ' ');
2290 if (html
.search(/^<p>(||\s||<br\s?\/?>|| )<\/p>$/i) !== -1) {
2294 // remove image resize
2295 html
= html
.replace(
2296 /<span(.*?)id="redactor-image-box"(.*?[^>])>([\w\W]*?)<img(.*?)><\/span>/gi,
2299 html
= html
.replace(/<span(.*?)id="redactor-image-resizer"(.*?[^>])>(.*?)<\/span>/gi,
2302 html
= html
.replace(/<span(.*?)id="redactor-image-editter"(.*?[^>])>(.*?)<\/span>/gi,
2305 html
= html
.replace(
2306 /<img(.*?)style="(.*?)opacity: 0\.5;(.*?)"(.*?)>/gi,
2307 '<img$1style="$2$3"$4>'
2310 var $div
= $('<div/>').html($.parseHTML(html
, document
, true));
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');
2322 $div
.find('.redactor-invisible-space, .redactor-unlink').each(function () {
2323 $(this).contents().unwrap();
2326 // remove span without attributes & span marker
2327 $div
.find('span, a').removeAttr('data-redactor-span data-redactor-style-cache').each(
2329 if (this.attributes
.length
=== 0) {
2330 $(this).contents().unwrap();
2334 // remove rel attribute from img
2335 $div
.find('img').removeAttr('rel');
2337 $div
.find('.redactor-selection-marker, #redactor-insert-marker').remove();
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>'
2350 html
= this.clean
.restoreFormTags(html
);
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>');
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
+ '">'
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">');
2371 // replace special characters
2373 '\u2122': '™',
2375 '\u2026': '…',
2376 '\u2014': '—',
2380 $.each(chars
, function (i
, s
) {
2381 html
= html
.replace(new RegExp(i
, 'g'), s
);
2384 html
= html
.replace(/&/g, '&');
2386 // remove empty paragpraphs
2387 //html = html.replace(/<p><\/p>/gi, "");
2390 html
= html
.replace(/\n{2,}/g, '\n');
2392 // remove all newlines
2393 if (this.opts
.removeNewlines
) {
2394 html
= html
.replace(/\r?\n/g, '');
2399 onPaste: function (html
, data
, insert
) {
2401 if (insert
!== true) {
2402 // remove google docs markers
2403 html
= html
.replace(/<b\sid="internal-source-marker(.*?)">([\w\W]*?)<\/b>/gi,
2406 html
= html
.replace(/<b(.*?)id="docs-internal-guid(.*?)">([\w\W]*?)<\/b>/gi,
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,
2415 html
= html
.replace(
2416 /<span[^>]*(font-style: italic; font-weight: 700|font-weight: 700; font-style: italic)[^>]*>([\w\W]*?)<\/span>/gi,
2419 html
= html
.replace(
2420 /<span[^>]*font-style: italic[^>]*>([\w\W]*?)<\/span>/gi,
2423 html
= html
.replace(
2424 /<span[^>]*font-weight: bold[^>]*>([\w\W]*?)<\/span>/gi,
2427 html
= html
.replace(
2428 /<span[^>]*font-weight: 700[^>]*>([\w\W]*?)<\/span>/gi,
2433 html
= html
.replace(/<o:p[^>]*>/gi, '');
2434 html
= html
.replace(/<\/o:p>/gi, '');
2436 var msword
= this.clean
.isHtmlMsWord(html
);
2438 html
= this.clean
.cleanMsWord(html
);
2442 html
= $.trim(html
);
2445 if (this.opts
.preSpaces
) {
2446 html
= html
.replace(/\t/g,
2447 new Array(this.opts
.preSpaces
+ 1).join(' ')
2453 html
= this.clean
.replaceBrToNl(html
);
2454 html
= this.clean
.removeTagsInsidePre(html
);
2458 if (insert
!== true) {
2459 html
= this.clean
.removeEmptyInlineTags(html
);
2461 if (data
.encode
=== false) {
2462 html
= html
.replace(/&/g
, '&');
2463 html
= this.clean
.convertTags(html
, data
);
2464 html
= this.clean
.getPlainText(html
);
2465 html
= this.clean
.reconvertTags(html
, data
);
2471 html
= this.clean
.replaceNbspToSpaces(html
);
2472 html
= this.clean
.getPlainText(html
);
2476 html
= html
.replace('\n', '<br>');
2480 html
= this.clean
.encodeHtml(html
);
2483 if (data
.paragraphize
) {
2485 html
= html
.replace(/ \n/g
, ' ');
2486 html
= html
.replace(/\n /g, ' ');
2488 html
= this.paragraphize
.load(html
);
2491 html
= html
.replace(/<p><\/p>/g, '');
2494 // remove paragraphs form lists (google docs bug)
2495 html
= html
.replace(/<li><p>/g, '<li>');
2496 html
= html
.replace(/<\/p><\/li>/g, '</li>');
2501 getCurrentType: function (html
, insert
) {
2502 var blocks
= this.selection
.blocks();
2508 line
: this.clean
.isHtmlLine(html
),
2509 blocks
: this.clean
.isHtmlBlocked(html
),
2518 if (blocks
.length
=== 1 && this.utils
.isCurrentOrParent([
2519 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'figcaption'
2522 data
.paragraphize
= false;
2523 data
.inline
= false;
2524 data
.images
= false;
2528 else if (this.opts
.type
=== 'inline' || this.opts
.enterKey
=== false) {
2529 data
.paragraphize
= false;
2533 else if (blocks
.length
=== 1 && this.utils
.isCurrentOrParent(['li'])) {
2536 data
.paragraphize
= false;
2537 data
.images
= false;
2539 else if (blocks
.length
=== 1 && this.utils
.isCurrentOrParent([
2540 'th', 'td', 'blockquote'
2543 data
.paragraphize
= false;
2546 else if (this.opts
.type
=== 'pre' || (blocks
.length
=== 1 && this.utils
.isCurrentOrParent(
2548 data
.inline
= false;
2552 data
.paragraphize
= false;
2553 data
.images
= false;
2557 if (data
.line
=== true) {
2558 data
.paragraphize
= false;
2561 if (insert
=== true) {
2568 isHtmlBlocked: function (html
) {
2569 var match1
= html
.match(new RegExp('</(' + this.opts
.blockTags
.join('|').toUpperCase() + ')>',
2572 var match2
= html
.match(new RegExp('<hr(.*?[^>])>', 'gi'));
2574 return (match1
=== null && match2
=== null) ? false : true;
2576 isHtmlLine: function (html
) {
2577 if (this.clean
.isHtmlBlocked(html
)) {
2581 var matchBR
= html
.match(/<br\s?\/?>/gi);
2582 var matchNL
= html
.match(/\n/gi);
2584 return (!matchBR
&& !matchNL
) ? true : false;
2586 isHtmlMsWord: function (html
) {
2588 /class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i);
2590 removeEmptyInlineTags: function (html
) {
2591 var tags
= this.opts
.inlineTags
;
2592 var $div
= $('<div/>').html($.parseHTML(html
, document
, true));
2595 var $spans
= $div
.find('span');
2596 var $tags
= $div
.find(tags
.join(','));
2598 $tags
.removeAttr('style');
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();
2609 $spans
.each(function () {
2610 var tagHtml
= $(this).html();
2611 if (this.attributes
.length
=== 0) {
2612 $(this).replaceWith(function () {
2613 return $(this).contents();
2621 html
= html
.replace('<!--?php', '<?php');
2622 html
= html
.replace('<!--?', '<?');
2623 html
= html
.replace('?-->', '?>');
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');
2636 var $div
= $('<div/>').html(html
);
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');
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
);
2651 parent
.parentNode
.insertBefore(paragraph
, parent
.nextSibling
);
2658 var lastList
= false;
2662 $div
.find('p[style]').each(function () {
2663 var matches
= $(this).attr('style').match(
2664 /mso\-list\:l([0-9]+)\slevel([0-9]+)/);
2667 var currentList
= parseInt(matches
[1]);
2668 var currentLevel
= parseInt(matches
[2]);
2669 var listType
= $(this).html().match(/^[\w]+\./) ? 'ol' : 'ul';
2671 var $li
= $('<li/>').html($(this).html());
2673 $li
.html($li
.html().replace(/^([\w\.]+)</, '<'));
2674 $li
.find('span:first').remove();
2676 if (currentLevel
== 1 && $.inArray(currentList
,
2679 var $list
= $('<' + listType
+ '/>').attr({
2680 'data-level': currentLevel
,
2681 'data-list': currentList
2683 $(this).replaceWith($list
);
2685 lastList
= currentList
;
2686 listsIds
.push(currentList
);
2689 if (currentLevel
> lastLevel
) {
2690 var $prevList
= $div
.find('[data-level="' + lastLevel
+ '"][data-list="' + lastList
+ '"]');
2691 var $lastList
= $prevList
;
2693 for (var i
= lastLevel
; i
< currentLevel
; i
++) {
2694 $list
= $('<' + listType
+ '/>');
2695 $list
.appendTo($lastList
.find('li').last());
2701 'data-level': currentLevel
,
2702 'data-list': currentList
2707 var $prevList
= $div
.find('[data-level="' + currentLevel
+ '"][data-list="' + currentList
+ '"]').last();
2709 $prevList
.append($li
);
2712 lastLevel
= currentLevel
;
2713 lastList
= currentList
;
2720 $div
.find('[data-level][data-list]').removeAttr('data-level data-list');
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>') {
2732 sibling
= list
[property
];
2741 replaceNbspToSpaces: function (html
) {
2742 return html
.replace(' ', ' ');
2744 replaceBrToNl: function (html
) {
2745 return html
.replace(/<br\s?\/?>/gi, '\n');
2747 replaceNlToBr: function (html
) {
2748 return html
.replace(/\n/g, '<br />');
2750 convertTags: function (html
, data
) {
2751 var $div
= $('<div>').html(html
);
2754 $div
.find('iframe').remove();
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
);
2764 if (data
.links
&& this.opts
.pasteLinks
) {
2765 $div
.find('a').each(function (i
, link
) {
2767 var tmp
= '#####[a href="' + link
.href
+ '"';
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
+ '"';
2776 link
.outerHTML
= tmp
+ ']#####' + link
.innerHTML
+ '#####[/a]#####';
2784 if (data
.images
&& this.opts
.pasteImages
) {
2785 html
= html
.replace(
2786 /<img(.*?)src="(.*?)"(.*?[^>])>/gi,
2787 '#####[img$1src="$2"$3]#####'
2792 if (this.opts
.pastePlainText
) {
2797 var blockTags
= (data
.lists
) ? ['ul', 'ol', 'li'] : this.opts
.pasteBlockTags
;
2800 if (data
.block
|| data
.lists
) {
2801 tags
= (data
.inline
) ? blockTags
.concat(this.opts
.pasteInlineTags
) : blockTags
;
2804 tags
= (data
.inline
) ? this.opts
.pasteInlineTags
: [];
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
] + '###'
2813 if (tags
[i
] === 'td' || tags
[i
] === 'th') {
2814 html
= html
.replace(new RegExp(
2815 '<' + tags
[i
] + '(.*?[^>])((colspan|rowspan)="(.*?[^>])")?(.*?[^>])>',
2817 ), '###' + tags
[i
] + ' $2###');
2819 else if (this.utils
.isInlineTag(tags
[i
])) {
2820 html
= html
.replace(
2821 new RegExp('<' + tags
[i
] + '([^>]*)class="([^>]*)"[^>]*>',
2824 '###' + tags
[i
] + ' class="$2"###'
2826 html
= html
.replace(new RegExp(
2827 '<' + tags
[i
] + '([^>]*)data-redactor-style-cache="([^>]*)"[^>]*>',
2829 ), '###' + tags
[i
] + ' cache="$2"###');
2830 html
= html
.replace(new RegExp('<' + tags
[i
] + '[^>]*>', 'gi'),
2831 '###' + tags
[i
] + '###'
2835 html
= html
.replace(new RegExp('<' + tags
[i
] + '[^>]*>', 'gi'),
2836 '###' + tags
[i
] + '###'
2844 reconvertTags: function (html
, data
) {
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'), '>');
2852 if (this.opts
.pastePlainText
) {
2856 var blockTags
= (data
.lists
) ? ['ul', 'ol', 'li'] : this.opts
.pasteBlockTags
;
2859 if (data
.block
|| data
.lists
) {
2860 tags
= (data
.inline
) ? blockTags
.concat(this.opts
.pasteInlineTags
) : blockTags
;
2863 tags
= (data
.inline
) ? this.opts
.pasteInlineTags
: [];
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
] + '>'
2873 for (var i
= 0; i
< len
; i
++) {
2874 html
= html
.replace(new RegExp('###' + tags
[i
] + '###', 'gi'),
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>'
2886 else if (this.utils
.isInlineTag(tags
[i
])) {
2888 var spanMarker
= (tags
[i
] === 'span') ? ' data-redactor-span="true"' : '';
2890 html
= html
.replace(new RegExp('###' + tags
[i
] + ' cache="(.*?[^#])"###',
2893 '<' + tags
[i
] + ' style="$1"' + spanMarker
+ ' data-redactor-style-cache="$1">'
2895 html
= html
.replace(
2896 new RegExp('###' + tags
[i
] + '\s?(.*?[^#])###', 'gi'),
2897 '<' + tags
[i
] + '$1>'
2905 cleanPre: function (block
) {
2906 block
= (typeof block
=== 'undefined') ? $(this.selection
.block()).closest('pre',
2907 this.core
.editor()[0]
2910 $(block
).find('br').replaceWith(function () {
2911 return document
.createTextNode('\n');
2914 $(block
).find('p').replaceWith(function () {
2915 return $(this).contents();
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, '');
2926 return $('<pre />').append(str
);
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');
2943 var tmp
= document
.createElement('div');
2944 tmp
.innerHTML
= html
;
2945 html
= tmp
.textContent
|| tmp
.innerText
;
2947 return $.trim(html
);
2949 savePreCode: function (html
) {
2950 html
= this.clean
.savePreFormatting(html
);
2951 html
= this.clean
.saveCodeFormatting(html
);
2952 html
= this.clean
.restoreSelectionMarkers(html
);
2956 savePreFormatting: function (html
) {
2957 var pre
= html
.match(/<pre(.*?)>([\w\W]*?)<\/pre>/gi);
2962 $.each(pre
, $.proxy(function (i
, s
) {
2964 var codeTag
= false;
2965 var contents
, attr1
, attr2
;
2967 if (s
.match(/<pre(.*?)>(([\n\r\s]+)?)<code(.*?)>/i)) {
2969 /<pre(.*?)>(([\n\r\s]+)?)<code(.*?)>([\w\W]*?)<\/code>(([\n\r\s]+)?)<\/pre>/i);
2977 arr
= s
.match(/<pre(.*?)>([\w\W]*?)<\/pre>/i);
2983 contents
= contents
.replace(/<br\s?\/?>/g, '\n');
2984 contents
= contents
.replace(/ /g, ' ');
2986 if (this.opts
.preSpaces
) {
2987 contents
= contents
.replace(/\t/g,
2988 new Array(this.opts
.preSpaces
+ 1).join(' ')
2992 contents
= this.clean
.encodeEntities(contents
);
2995 contents
= contents
.replace(/\$/g, '$');
2998 html
= html
.replace(s
,
2999 '<pre' + attr1
+ '><code' + attr2
+ '>' + contents
+ '</code></pre>'
3003 html
= html
.replace(s
,
3004 '<pre' + attr1
+ '>' + contents
+ '</pre>'
3012 saveCodeFormatting: function (html
) {
3013 var code
= html
.match(/<code(.*?)>([\w\W]*?)<\/code>/gi);
3014 if (code
=== null) {
3018 $.each(code
, $.proxy(function (i
, s
) {
3019 var arr
= s
.match(/<code(.*?)>([\w\W]*?)<\/code>/i);
3021 arr
[2] = arr
[2].replace(/ /g, ' ');
3022 arr
[2] = this.clean
.encodeEntities(arr
[2]);
3023 arr
[2] = arr
[2].replace(/\$/g, '$');
3025 html
= html
.replace(s
, '<code' + arr
[1] + '>' + arr
[2] + '</code>');
3031 restoreSelectionMarkers: function (html
) {
3032 html
= html
.replace(
3033 /<span id="selection-marker-([0-9])" class="redactor-selection-marker"><\/span>/g,
3034 '<span id="selection-marker-$1" class="redactor-selection-marker"></span>'
3039 saveFormTags: function (html
) {
3042 restoreFormTags: function (html
) {
3043 return html
.replace(
3044 /<section(.*?) rel="redactor-form-tag"(.*?)>([\w\W]*?)<\/section>/gi,
3045 '<form$1$2>$3</form>'
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
);
3057 encodeEntities: function (str
) {
3058 str
= String(str
).replace(/&/g, '&').replace(/</g, '<').replace(/>/g,
3060 ).replace(/"/g, '"');
3061 str
= str
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g
,
3063 ).replace(/"/g, '"');
3067 stripTags: function (input, denied) {
3068 if (typeof denied === 'undefined') {
3069 return input.replace(/(<([^>]+)>)/gi, '');
3072 var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
3074 return input.replace(tags, function ($0, $1) {
3075 return denied.indexOf($1.toLowerCase()) === -1 ? $0 : '';
3078 removeMarkers: function (html) {
3079 return html.replace(
3080 /<span(.*?[^>]?)class="redactor
-selection
-marker
"(.*?[^>]?)>([\w\W]*?)<\/span>/gi,
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, '');
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, '');
3110 normalizeCurrentHeading: function () {
3111 var heading = this.selection.block();
3112 if (this.utils.isCurrentOrParentHeader() && heading) {
3113 heading.normalize();
3124 start: function (html) {
3125 html = $.trim(html);
3126 html = html.replace(
3127 /^(<span id="selection
-marker
-1" class="redactor
-selection
-marker
"><\/span>)/,
3131 html = this.clean.onSet(html);
3133 html = html.replace(
3134 /<p><span id="selection
-marker
-1" class="redactor
-selection
-marker
"><\/span><\/p>/,
3138 this.events.stopDetectChanges();
3139 this.core.editor().html(html);
3141 this.observe.load();
3142 this.events.startDetectChanges();
3144 set: function (html, options) {
3145 html = $.trim(html);
3147 options = options || {};
3150 if (options.start) {
3151 this.start = options.start;
3155 if (this.opts.type === 'textarea') {
3156 html = this.clean.onSet(html);
3158 else if (this.opts.type === 'div' && html === '') {
3159 html = this.opts.emptyHtml;
3162 this.core.editor().html(html);
3164 if (this.opts.type === 'textarea') {
3169 if (this.opts.type === 'textarea') {
3170 return this.core.textarea().val();
3173 var html = this.core.editor().html();
3176 html = this.clean.onGet(html);
3182 if (!this.code.syncFire) {
3186 var html = this.core.editor().html();
3187 var htmlCleaned = this.code.cleaned(html);
3189 // is there a need to synchronize
3190 if (this.code.isSync(htmlCleaned)) {
3196 this.code.html = htmlCleaned;
3198 if (this.opts.type !== 'textarea') {
3199 this.core.callback('sync', html);
3200 this.core.callback('change', html);
3204 if (this.opts.type === 'textarea') {
3205 setTimeout($.proxy(function () {
3206 this.code.startSync(html);
3211 startSync: function (html) {
3212 // before clean callback
3213 html = this.core.callback('syncBefore', html);
3216 html = this.clean.onSync(html);
3219 this.core.textarea().val(html);
3221 // after sync callback
3222 this.core.callback('sync', html);
3225 if (this.start === false) {
3226 this.core.callback('change', html);
3231 isSync: function (htmlCleaned) {
3232 var html = (this.code.html !== false) ? this.code.html : false;
3234 return (html !== false && html === htmlCleaned);
3236 cleaned: function (html) {
3237 html = html.replace(/\u200B/g, '');
3238 return this.clean.removeMarkers(html);
3248 return this.$editor.attr('id');
3250 element: function () {
3251 return this.$element;
3253 editor: function () {
3254 return (typeof this.$editor === 'undefined') ? $() : this.$editor;
3256 textarea: function () {
3257 return this.$textarea;
3260 return (this.opts.type === 'textarea') ? this.$box : this.$element;
3262 toolbar: function () {
3263 return (this.$toolbar) ? this.$toolbar : false;
3266 return (this.$air) ? this.$air : false;
3268 object: function () {
3269 return $.extend({}, this);
3271 structure: function () {
3272 this.core.editor().toggleClass('redactor-structure');
3274 addEvent: function (name) {
3275 this.core.event = name;
3277 getEvent: function () {
3278 return this.core.event;
3280 callback: function (type, e, data) {
3281 var eventNamespace = 'redactor';
3282 var returnValue = false;
3283 var events = $._data(this.core.element()[0], 'events');
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] : [
3295 returnValue = (typeof args === 'undefined') ? handler.call(this,
3297 ) : handler.call(this, e, args);
3307 if (typeof this.opts.callbacks[type] === 'undefined') {
3308 return (typeof data === 'undefined') ? e : data;
3312 var callback = this.opts.callbacks[type];
3314 if ($.isFunction(callback)) {
3315 return (typeof data === 'undefined') ? callback.call(this,
3317 ) : callback.call(this, e, data);
3320 return (typeof data === 'undefined') ? e : data;
3323 destroy: function () {
3324 this.opts.destroyed = true;
3326 this.core.callback('destroy');
3329 $('#redactor-voice-' + this.uuid).remove();
3331 this.core.editor().removeClass(
3332 'redactor-in redactor-styles redactor-structure redactor-layer-img-edit');
3335 this.core.editor().off('keydown.redactor-remove-textnode');
3338 this.core.editor().off('.redactor-observe.' + this.uuid);
3340 // off events and remove data
3341 this.$element.off('.redactor').removeData('redactor');
3342 this.core.editor().off('.redactor');
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);
3352 $(this.opts.toolbarFixedTarget).off('scroll.redactor.' + this.uuid);
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);
3366 this.$element.off('click.redactor-click-to-edit');
3367 this.$element.removeClass('redactor-click-to-edit');
3370 this.core.editor().removeClass('redactor-layer');
3371 this.core.editor().removeAttr('contenteditable');
3373 var html = this.code.get();
3375 if (this.opts.toolbar && this.$toolbar) {
3377 this.$toolbar.find('a').each(function () {
3379 if ($el.data('dropdown')) {
3380 $el.data('dropdown').remove();
3381 $el.data('dropdown', {});
3386 if (this.opts.type === 'textarea') {
3387 this.$box.after(this.$element);
3389 this.$element.val(html).show();
3392 if (this.opts.toolbar && this.$toolbar) {
3393 this.$toolbar.remove();
3397 if (this.$modalBox) {
3398 this.$modalBox.remove();
3401 if (this.$modalOverlay) {
3402 this.$modalOverlay.remove();
3405 // hide link's tooltip
3406 $('.redactor-link-tooltip').remove();
3412 detect: function () {
3416 isWebkit: function () {
3417 return /webkit/.test(this.opts.userAgent);
3419 isFirefox: function () {
3420 return this.opts.userAgent.indexOf('firefox') > -1;
3422 isIe: function (v) {
3423 if (document.documentMode || /Edge/.test(navigator.userAgent)) {
3428 ie = RegExp('msie' + (!isNaN(v) ? ('\\s' + v) : ''),
3430 ).test(navigator.userAgent);
3433 ie = !!navigator.userAgent.match(/Trident.*rv[ :]*11\./);
3438 isMobile: function () {
3439 return /(iPhone|iPod|BlackBerry|Android)/.test(navigator.userAgent);
3441 isDesktop: function () {
3442 return !/(iPhone|iPod|iPad|BlackBerry|Android)/.test(navigator.userAgent);
3444 isIpad: function () {
3445 return /iPad/.test(navigator.userAgent);
3452 dropdown: function () {
3458 getDropdown: function () {
3459 return this.dropdown.active;
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];
3467 var item = this.dropdown.buildItem(btnName, btnObject);
3469 this.observe.addDropdown($(item), btnName, btnObject);
3470 fragment.appendChild(item);
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) {
3483 $dropdown[0].rel = name;
3484 $dropdown[0].appendChild(fragment);
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);
3494 if (btnName.toLowerCase().indexOf('divider') === 0) {
3495 itemContainer.classList.add('redactor-dropdown-divider');
3497 return itemContainer;
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();
3506 this.dropdown.buildClick(event, btnName, btnObject);
3509 return itemContainer;
3511 buildClick: function (e, btnName, btnObject) {
3512 if ($(e.target).hasClass('redactor-dropdown-link-inactive')) {
3516 var command = this.dropdown.buildCommand(btnObject);
3518 if (typeof btnObject.args !== 'undefined') {
3519 this.button.toggle(e,
3527 this.button.toggle(e, btnName, command.type, command.callback);
3530 buildCommand: function (btnObject) {
3532 command.type = 'func';
3533 command.callback = btnObject.func;
3535 if (btnObject.command) {
3536 command.type = 'command';
3537 command.callback = btnObject.command;
3539 else if (btnObject.dropdown) {
3540 command.type = 'dropdown';
3541 command.callback = btnObject.dropdown;
3546 show: function (e, key) {
3547 if (this.detect.isDesktop()) {
3548 this.core.editor().focus();
3551 this.dropdown.hideAll(false, key);
3553 this.dropdown.key = key;
3554 this.dropdown.button = this.button.get(this.dropdown.key);
3556 require(['Ui/SimpleDropdown'], (function(UiSimpleDropdown) {
3557 var dropdownId = this.dropdown.button[0].id;
3559 UiSimpleDropdown.toggleDropdown(dropdownId);
3560 if (UiSimpleDropdown.isOpen(dropdownId)) {
3561 this.dropdown.active = $(UiSimpleDropdown.getDropdownMenu(dropdownId));
3563 this.core.callback('dropdownShow', {
3564 dropdown: this.dropdown.active,
3565 key: this.dropdown.key,
3566 button: this.dropdown.button
3569 this.button.setActive(this.dropdown.key);
3570 this.dropdown.button.addClass('dropact').attr('aria-expanded', true);
3572 this.dropdown.enableCallback();
3575 this.dropdown.hide();
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
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) {
3600 if (this.dropdown.button[0].rel === key) {
3604 this.core.callback('dropdownHide', this.dropdown.active);
3606 var id = this.dropdown.button[0].id;
3607 require(['Ui/SimpleDropdown'], function(UiSimpleDropdown) {
3608 UiSimpleDropdown.close(id);
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;
3620 events: function () {
3626 stopDetectChanges: function () {
3627 this.events.stopChanges = true;
3629 startDetectChanges: function () {
3631 setTimeout(function () {
3632 self.events.stopChanges = false;
3635 dragover: function (e) {
3637 e.stopPropagation();
3639 if (e.target.tagName === 'IMG') {
3640 $(e.target).addClass('redactor-image-dragover');
3644 dragleave: function (e) {
3645 // remove image dragover
3646 this.core.editor().find('img').removeClass('redactor-image-dragover');
3648 drop: function (e) {
3649 e = e.originalEvent || e;
3651 // remove image dragover
3652 this.core.editor().find('img').removeClass('redactor-image-dragover');
3654 if (this.opts.type === 'inline' || this.opts.type === 'pre') {
3659 if (window.FormData === undefined || !e.dataTransfer) {
3663 if (e.dataTransfer.files.length === 0) {
3664 return this.events.onDrop(e);
3667 this.events.onDropUpload(e);
3670 this.core.callback('drop', e);
3673 click: function (e) {
3674 var event = this.core.getEvent();
3675 var type = (event === 'click' || event === 'arrow') ? false : 'click';
3677 this.core.addEvent(type);
3678 this.utils.disableSelectAll();
3679 this.core.callback('click', e);
3681 focus: function (e) {
3682 if (this.rtePaste) {
3686 if (this.events.isCallback('focus')) {
3687 this.core.callback('focus', e);
3690 this.events.focused = true;
3691 this.events.blured = false;
3694 if (this.selection.current() === false) {
3695 var sel = this.selection.get();
3696 var range = this.selection.range(sel);
3698 range.setStart(this.core.editor()[0], 0);
3699 range.setEnd(this.core.editor()[0], 0);
3700 this.selection.update(sel, range);
3704 blur: function (e) {
3705 if (this.start || this.rtePaste) {
3709 if ($(e.target).closest('#' + this.core.id() + ', .redactor-toolbar, .redactor-dropdown, #redactor-modal-box').length !== 0) {
3713 if (!this.events.blured && this.events.isCallback('blur')) {
3714 this.core.callback('blur', e);
3717 this.events.focused = false;
3718 this.events.blured = true;
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);
3729 scrollTimer = setTimeout($.proxy(function () {
3730 this.events.imageEditing = false;
3737 this.core.editor().on('dragover.redactor dragenter.redactor',
3738 $.proxy(this.events.dragover, this)
3740 this.core.editor().on('dragleave.redactor',
3741 $.proxy(this.events.dragleave, this)
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));
3750 $(document).on('mousedown.redactor-blur.' + this.uuid,
3751 $.proxy(this.events.blur, this)
3754 this.events.touchImageEditing();
3756 this.events.createObserver();
3757 this.events.setupObserver();
3760 createObserver: function () {
3762 this.events.observer = new MutationObserver(function (mutations) {
3763 mutations.forEach($.proxy(self.events.iterateObserver, self));
3767 iterateObserver: function (mutation) {
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')) {
3777 this.observe.load();
3778 this.events.changeHandler();
3781 setupObserver: function () {
3782 this.events.observer.observe(this.core.editor()[0], {
3786 characterData: true,
3787 characterDataOldValue: true
3790 changeHandler: function () {
3791 if (this.events.stopChanges) {
3798 onDropUpload: function (e) {
3800 e.stopPropagation();
3802 if ((!this.opts.dragImageUpload && !this.opts.dragFileUpload) || (this.opts.imageUpload === null && this.opts.fileUpload === null)) {
3806 if (e.target.tagName === 'IMG') {
3807 this.events.dropImage = e.target;
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);
3816 onDrop: function (e) {
3817 this.core.callback('drop', e);
3819 isCallback: function (name) {
3820 return (typeof this.opts.callbacks[name] !== 'undefined' && $.isFunction(this.opts.callbacks[name]));
3824 stopDetect: function () {
3825 this.events.stopDetectChanges();
3827 startDetect: function () {
3828 this.events.startDetectChanges();
3834 // =file -- UNSUPPORTED MODULE
3838 show: function () {},
3839 insert: function () {},
3840 release: function () {},
3841 text: function (json) {}
3846 focus: function () {
3848 start: function () {
3849 this.core.editor().focus();
3851 if (this.opts.type === 'inline') {
3855 var $first = this.focus.first();
3856 if ($first !== false) {
3857 this.caret.start($first);
3861 this.core.editor().focus();
3863 var last = (this.opts.inline) ? this.core.editor() : this.focus.last();
3864 if (last.length === 0) {
3868 // get inline last node
3869 var lastNode = this.focus.lastChild(last);
3870 if (!this.detect.isWebkit() && lastNode !== false) {
3871 this.caret.end(lastNode);
3874 var sel = this.selection.get();
3875 var range = this.selection.range(sel);
3877 if (range !== null) {
3878 range.selectNodeContents(last[0]);
3879 range.collapse(false);
3881 this.selection.update(sel, range);
3884 this.caret.end(last);
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)) {
3895 if ($first[0].tagName === 'UL' || $first[0].tagName === 'OL') {
3896 return $first.find('li').first();
3903 return this.core.editor().children().last();
3905 lastChild: function (last) {
3906 var lastNode = last[0].lastChild;
3908 return (lastNode !== null && this.utils.isInlineTag(lastNode.tagName)) ? lastNode : false;
3911 return (this.core.editor()[0] === document.activeElement);
3917 image: function () {
3920 return !(!this.opts.imageUpload || !this.opts.imageUpload && !this.opts.s3);
3924 this.modal.load('image', this.lang.get('image'), 700);
3927 this.upload.init('#redactor-modal-image-droparea',
3928 this.opts.imageUpload,
3934 insert: function (json, direct, e) {
3938 if (typeof json.error !== 'undefined') {
3940 this.events.dropImage = false;
3941 this.core.callback('imageUploadError', json, e);
3946 if (this.events.dropImage !== false) {
3947 $img = $(this.events.dropImage);
3949 this.core.callback('imageDelete', $img[0].src, $img);
3951 $img.attr('src', json.url);
3953 this.events.dropImage = false;
3954 this.core.callback('imageUpload', $img, json);
3958 var $figure = $('<' + this.opts.imageTag + '>');
3961 $img.attr('src', json.url);
3964 var id = (typeof json.id === 'undefined') ? '' : json.id;
3965 var type = (typeof json.s3 === 'undefined') ? 'image' : 's3';
3966 $img.attr('data-' + type, id);
3968 $figure.append($img);
3970 var pre = this.utils.isTag(this.selection.current(), 'pre');
3973 this.marker.remove();
3975 var node = this.insert.nodeToPoint(e, this.marker.get());
3976 var $next = $(node).next();
3978 this.selection.restore();
3984 if (typeof $next !== 'undefined' && $next.length !== 0 && $next[0].tagName === 'IMG') {
3986 this.core.callback('imageDelete', $next[0].src, $next);
3989 $next.closest('figure, p', this.core.editor()[0]).replaceWith(
3991 this.caret.after($figure);
3995 $(pre).after($figure);
3998 this.insert.node($figure);
4001 this.caret.after($figure);
4013 $(pre).after($figure);
4016 this.insert.node($figure);
4019 this.caret.after($figure);
4022 this.events.dropImage = false;
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, '');
4029 if (isNextEmpty === '') {
4030 $(nextNode).remove();
4033 if ($nextFigure.length === 1 && $nextFigure[0].tagName === 'FIGURE' && isNextFigureEmpty === '') {
4034 $nextFigure.remove();
4037 if (direct !== null) {
4038 this.core.callback('imageUpload', $img, json);
4041 this.core.callback('imageInserted', $img, json);
4044 setEditable: function ($image) {
4045 $image.on('dragstart', function (e) {
4049 if (this.opts.imageResizable) {
4050 var handler = $.proxy(function (e) {
4051 this.observe.image = $image;
4052 this.image.resizer = this.image.loadEditableControls($image);
4054 $(document).on('mousedown.redactor-image-resize-hide.' + this.uuid,
4055 $.proxy(this.image.hideResize, this)
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);
4069 $image.off('mousedown.redactor').on('mousedown.redactor',
4070 $.proxy(this.image.hideResize, this)
4072 $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor',
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);
4089 setResizable: function (e, $image) {
4092 this.image.resizeHandle = {
4096 ratio: $image.width() / $image.height(),
4100 e = e.originalEvent || e;
4102 if (e.targetTouches) {
4103 this.image.resizeHandle.x = e.targetTouches[0].pageX;
4104 this.image.resizeHandle.y = e.targetTouches[0].pageY;
4107 this.image.startResize();
4109 startResize: function () {
4110 $(document).on('mousemove.redactor-image-resize touchmove.redactor-image-resize',
4111 $.proxy(this.image.moveResize, this)
4113 $(document).on('mouseup.redactor-image-resize touchend.redactor-image-resize',
4114 $.proxy(this.image.stopResize, this)
4117 moveResize: function (e) {
4120 e = e.originalEvent || e;
4122 var height = this.image.resizeHandle.h;
4124 if (e.targetTouches) height += (e.targetTouches[0].pageY - this.image.resizeHandle.y); else height += (e.pageY - this.image.resizeHandle.y);
4126 var width = Math.round(height * this.image.resizeHandle.ratio);
4128 if (height < 50 || width < 100) return;
4129 if (this.core.editor().width() <= width) return;
4131 this.image.resizeHandle.el.attr({
4135 this.image.resizeHandle.el.width(width);
4136 this.image.resizeHandle.el.height(height);
4140 stopResize: function () {
4141 this.handle = false;
4142 $(document).off('.redactor-image-resize');
4144 this.image.hideResize();
4146 hideResize: function (e) {
4147 if (e && $(e.target).closest('#redactor-image-box',
4152 if (e && e.target.tagName == 'IMG') {
4153 var $image = $(e.target);
4156 var imageBox = this.$editor.find('#redactor-image-box');
4157 if (imageBox.length === 0) return;
4159 $('#redactor-image-editter').remove();
4160 $('#redactor-image-resizer').remove();
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
4169 imageBox.css('margin', '');
4170 imageBox.find('img').css('opacity', '');
4171 imageBox.replaceWith(function () {
4172 return $(this).contents();
4175 $(document).off('mousedown.redactor-image-resize-hide.' + this.uuid);
4177 if (typeof this.image.resizeHandle !== 'undefined') {
4178 this.image.resizeHandle.el.attr('rel',
4179 this.image.resizeHandle.el.attr('style')
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>');
4188 if (!this.detect.isDesktop()) {
4195 imageResizer.attr('contenteditable', false);
4196 imageBox.append(imageResizer);
4197 imageBox.append($image);
4199 return imageResizer;
4202 imageBox.append($image);
4206 loadEditableControls: function ($image) {
4207 if ($('#redactor-image-box').length !== 0) {
4211 var imageBox = $('<span id="redactor
-image
-box
" data-redactor="verified
">');
4212 imageBox.css('float', $image.css('float')).attr('contenteditable', false);
4214 if ($image[0].style.margin != 'auto') {
4216 marginTop: $image[0].style.marginTop,
4217 marginBottom: $image[0].style.marginBottom,
4218 marginLeft: $image[0].style.marginLeft,
4219 marginRight: $image[0].style.marginRight
4222 $image.css('margin', '');
4231 $image.css('opacity', '.5').after(imageBox);
4233 if (this.opts.imageEditable) {
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);
4243 imageBox.append(this.image.editter);
4245 // position correction
4246 var editerWidth = this.image.editter.innerWidth();
4247 this.image.editter.css('margin-left', '-' + editerWidth / 2 + 'px');
4250 return this.image.loadResizableControls($image, imageBox);
4253 showEdit: function ($image) {
4254 if (this.events.imageEditing) {
4258 this.observe.image = $image;
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;
4264 this.modal.load('image-edit', this.lang.get('edit'), 705);
4266 this.image.buttonDelete = this.modal.getDeleteButton().text(this.lang.get(
4268 this.image.buttonSave = this.modal.getActionButton().text(this.lang.get('save'));
4270 this.image.buttonDelete.on('click', $.proxy(this.image.remove, this));
4271 this.image.buttonSave.on('click', $.proxy(this.image.update, this));
4273 if (this.opts.imageCaption === false) {
4274 $('#redactor-image-caption').val('').hide().prev().hide();
4277 var $parent = $image.closest(this.opts.imageTag, this.$editor[0]);
4278 var $ficaption = $parent.find('figcaption');
4279 if ($ficaption !== 0) {
4281 $('#redactor-image-caption').val($ficaption.text()).show();
4285 if (!this.opts.imagePosition) {
4286 $('.redactor-image-position-option').hide();
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);
4295 $('#redactor-image-preview').html($('<img src="' + $image.attr('src
') + '" style="max
-width
: 100%;">'));
4296 $('#redactor-image-title').val($image.attr('alt'));
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);
4305 // hide link's tooltip
4306 $('.redactor-link-tooltip').remove();
4311 if (this.detect.isDesktop()) {
4312 $('#redactor-image-title').focus();
4316 update: function () {
4317 var $image = this.observe.image;
4318 var $link = $image.closest('a', this.core.editor()[0]);
4320 var title = $('#redactor-image-title').val().replace(/(<([^>]+)>)/ig, '');
4321 $image.attr('alt', title).attr('title', title);
4323 this.image.setFloating($image);
4326 var link = $.trim($('#redactor-image-link').val()).replace(/(<([^>]+)>)/ig, '');
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');
4333 if (link.search(re) === -1 && link.search(re2) === 0 && this.opts.linkProtocol) {
4334 link = this.opts.linkProtocol + '://' + link;
4337 var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false;
4339 if ($link.length === 0) {
4340 var a = $('<a href="' + link + '" id="redactor
-img
-tmp
">' + this.utils.getOuterHtml(
4343 a.attr('target', '_blank');
4346 $image = $image.replaceWith(a);
4347 $link = this.core.editor().find('#redactor-img-tmp');
4348 $link.removeAttr('id');
4351 $link.attr('href', link);
4353 $link.attr('target', '_blank');
4356 $link.removeAttr('target');
4360 else if ($link.length !== 0) {
4361 $link.replaceWith(this.utils.getOuterHtml($image));
4364 this.image.addCaption($image, $link);
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();
4376 var imageFloat = '';
4377 var imageDisplay = '';
4378 var imageMargin = '';
4383 imageFloat = 'left';
4384 imageMargin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0';
4387 imageFloat = 'right';
4388 imageMargin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin;
4392 if ($figure.length !== 0) {
4393 textAlign = 'center';
4396 imageDisplay = 'block';
4397 imageMargin = 'auto';
4404 'float': imageFloat,
4405 'display': imageDisplay,
4406 'margin': imageMargin,
4407 'text-align': textAlign
4409 $container.attr('rel', $image.attr('style'));
4411 addCaption: function ($image, $link) {
4412 var caption = $('#redactor-image-caption').val();
4414 var $target = ($link.length !== 0) ? $link : $image;
4415 var $figcaption = $target.next();
4417 if ($figcaption.length === 0 || $figcaption[0].tagName !== 'FIGCAPTION') {
4418 $figcaption = false;
4421 if (caption !== '') {
4422 if ($figcaption === false) {
4423 $figcaption = $('<figcaption />').text(caption);
4424 $target.after($figcaption);
4427 $figcaption.text(caption);
4430 else if ($figcaption !== false) {
4431 $figcaption.remove();
4434 remove: function (e, $image, index) {
4435 $image = (typeof $image === 'undefined') ? $(this.observe.image) : $image;
4437 // delete from modal
4438 if (typeof e !== 'boolean') {
4442 this.events.stopDetectChanges();
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();
4449 var imageDeleteStop = this.core.callback('imageDelete', e, $image[0]);
4450 if (imageDeleteStop === false) {
4451 if (e) e.preventDefault();
4455 if ($('#redactor-image-box').length !== 0) {
4456 $parent = $('#redactor-image-box').parent();
4460 if ($figure.length !== 0) {
4461 $prev = $figure.prev();
4462 $next = $figure.next();
4465 else if ($link.length !== 0) {
4466 $parent = $link.parent();
4473 $('#redactor-image-box').remove();
4476 if ($next && $next.length !== 0) {
4477 this.caret.start($next);
4479 else if ($prev && $prev.length !== 0) {
4480 this.caret.end($prev);
4484 if (typeof e !== 'boolean') {
4488 this.utils.restoreScroll();
4489 this.observe.image = false;
4490 this.events.startDetectChanges();
4498 indent: function () {
4500 increase: function () {
4501 if (!this.list.get()) {
4505 var $current = $(this.selection.current()).closest('li');
4506 var $list = $current.closest('ul, ol', this.core.editor()[0]);
4508 var $li = $current.closest('li');
4509 var $prev = $li.prev();
4510 if ($prev.length === 0 || $prev[0].tagName !== 'LI') {
4516 if (this.utils.isCollapsed()) {
4517 var listTag = $list[0].tagName;
4518 var $newList = $('<' + listTag + ' />');
4520 this.selection.save();
4522 var $ol = $prev.find('ol').first();
4523 if ($ol.length === 1) {
4524 $ol.append($current);
4527 var listTag = $list[0].tagName;
4528 var $newList = $('<' + listTag + ' />');
4529 $newList.append($current);
4530 $prev.append($newList);
4533 this.selection.restore();
4536 document.execCommand('indent');
4539 this.selection.save();
4540 this.indent.removeEmpty();
4541 this.indent.normalize();
4542 this.selection.restore();
4545 decrease: function () {
4546 if (!this.list.get()) {
4550 var $current = $(this.selection.current()).closest('li');
4551 var $list = $current.closest('ul, ol', this.core.editor()[0]);
4555 document.execCommand('outdent');
4557 var $item = $(this.selection.current()).closest('li', this.core.editor()[0]);
4559 if (this.utils.isCollapsed()) {
4560 this.indent.repositionItem($item);
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') {
4573 this.selection.save();
4574 this.indent.removeEmpty();
4575 this.indent.normalize();
4576 this.selection.restore();
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);
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]);
4590 this.selection.restore();
4593 normalize: function () {
4594 this.core.editor().find('li').each($.proxy(function (i, s) {
4599 if (this.opts.keepStyleAttr.length !== 0) {
4600 filter = ',' + this.opts.keepStyleAttr.join(',');
4603 $el.find(this.opts.inlineTags.join(',')).not('img' + filter).removeAttr(
4606 var $parent = $el.parent();
4607 if ($parent.length !== 0 && $parent[0].tagName === 'LI') {
4612 var $next = $el.next();
4613 if ($next.length !== 0 && ($next[0].tagName === 'UL' || $next[0].tagName === 'OL')) {
4620 removeEmpty: function ($list) {
4621 var $lists = this.core.editor().find('ul, ol');
4622 var $items = this.core.editor().find('li');
4624 $items.each($.proxy(function (i, s) {
4625 this.indent.removeItemEmpty(s);
4629 $lists.each($.proxy(function (i, s) {
4630 this.indent.removeItemEmpty(s);
4634 $items.each($.proxy(function (i, s) {
4635 this.indent.removeItemEmpty(s);
4639 removeItemEmpty: function (s) {
4640 var html = s.innerHTML.replace(/[\t\s\n]/g, '');
4641 html = html.replace(/<span><\/span>/g, '');
4651 inline: function () {
4653 format: function (tag, attr, value, type) {
4654 // Stop formatting pre/code
4655 if (this.utils.isCurrentOrParent(['PRE', 'CODE'])) return;
4658 var params = this.inline.getParams(attr, value, type);
4661 tag = this.inline.arrangeTag(tag);
4665 (this.utils.isCollapsed()) ? this.inline.formatCollapsed(tag,
4667 ) : this.inline.formatUncollapsed(tag, params);
4669 formatCollapsed: function (tag, params) {
4671 var inline = this.selection.inline();
4674 var currentTag = inline.tagName.toLowerCase();
4675 if (currentTag === tag) {
4677 if (this.utils.isEmpty(inline.innerHTML)) {
4678 this.caret.after(inline);
4681 // not empty = break
4683 var $first = this.inline.insertBreakpoint(inline,
4686 this.caret.after($first);
4689 else if ($(inline).closest(tag).length === 0) {
4690 newInline = this.inline.insertInline(tag);
4691 newInline = this.inline.setParams(newInline, params);
4694 var $first = this.inline.insertBreakpoint(inline, currentTag);
4695 this.caret.after($first);
4699 newInline = this.inline.insertInline(tag);
4700 newInline = this.inline.setParams(newInline, params);
4703 formatUncollapsed: function (tag, params) {
4704 this.selection.save();
4706 var nodes = this.inline.getClearedNodes();
4707 this.inline.setNodesStriked(nodes, tag, params);
4709 this.selection.restore();
4711 document.execCommand('strikethrough');
4713 this.selection.saveInstant();
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];
4722 strike = document.createElement('strike');
4723 element.parentNode.insertBefore(strike, element);
4724 strike.appendChild(element);
4726 // Remove the bogus style attribute.
4727 element.style.removeProperty('text-decoration');
4731 this.core.editor().find('strike').each(function () {
4732 var $el = self.utils.replaceToTag(this, tag);
4733 self.inline.setParams($el[0], params);
4735 var $inside = $el.find(tag);
4736 var $parent = $el.parent();
4737 var $parentAround = $parent.parent();
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(); });
4748 if ($inside.length !== 0) {
4749 self.inline.cleanInsideOrParent($inside, params);
4753 if ($parent.html() == $el[0].outerHTML) {
4754 self.inline.cleanInsideOrParent($parent, params);
4757 // bugfix: remove empty inline tags after selection
4758 if (self.detect.isFirefox()) {
4759 self.core.editor().find(tag + ':empty').remove();
4763 this.selection.restoreInstant();
4765 cleanInsideOrParent: function ($el, params) {
4767 for (var key in params.data) {
4768 this.inline.removeSpecificAttr($el, key, params.data[key]);
4772 getClearedNodes: function () {
4773 var nodes = this.selection.nodes();
4775 var len = nodes.length;
4779 for (var i = 0; i < len; i++) {
4780 if ($(nodes[i]).hasClass('redactor-selection-marker')) {
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]);
4795 isConvertableAttr: function (node, name, value) {
4796 var nodeAttrValue = $(node).attr(name);
4797 if (nodeAttrValue) {
4798 if (name === 'style') {
4799 value = $.trim(value).replace(/;$/, '');
4801 var rules = value.split(';');
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]);
4808 if (ruleName.search(/color/) !== -1) {
4809 var val = $(node).css(ruleName);
4810 if (val && (val === ruleValue || this.utils.rgb2hex(
4811 val) === ruleValue)) {
4815 else if ($(node).css(ruleName) === ruleValue) {
4820 if (count === rules.length) {
4824 else if (nodeAttrValue === value) {
4832 isConvertable: function (node, nodeTag, tag, params) {
4833 if (nodeTag === tag) {
4836 for (var key in params.data) {
4837 count += this.inline.isConvertableAttr(node,
4843 if (count === Object.keys(params.data).length) {
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;
4858 var parent = nodes[i].parentNode;
4859 var parentTag = (parent && parent.tagName) ? parent.tagName.toLowerCase() : undefined;
4861 var convertable = this.inline.isConvertable(parent,
4867 var $el = $(parent).replaceWith(function () {
4868 return $('<strike>').append($(this).contents());
4871 $el.attr('data-redactor-inline-converted');
4874 var convertable = this.inline.isConvertable(nodes[i],
4880 var $el = $(nodes[i]).replaceWith(function () {
4881 return $('<strike>').append($(this).contents());
4886 insertBreakpoint: function (inline, currentTag) {
4887 var breakpoint = document.createElement('span');
4888 breakpoint.id = 'redactor-inline-breakpoint';
4889 breakpoint = this.insert.node(breakpoint);
4891 var end = this.utils.isEndOfElement(inline);
4892 var code = this.utils.getOuterHtml(inline);
4893 var endTag = (end) ? '' : '<' + currentTag + '>';
4895 code = code.replace(/<span id="redactor
-inline
-breakpoint
"><\/span>/i,
4896 '</' + currentTag + '>' + endTag
4899 var $code = $(code);
4900 $(inline).replaceWith($code);
4902 if (endTag !== '') {
4903 this.utils.cloneAttributes(inline, $code.last());
4906 return $code.first();
4908 insertInline: function (tag) {
4909 var node = document.createElement(tag);
4911 this.insert.node(node);
4912 this.caret.start(node);
4916 arrangeTag: function (tag) {
4929 'strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub'
4932 tag = tag.toLowerCase();
4934 for (var i = 0; i < tags.length; i++) {
4935 if (tag === tags[i]) {
4942 getStyleParams: function (params) {
4944 var rules = params.trim().replace(/;$/, '').split(';');
4945 for (var i = 0; i < rules.length; i++) {
4946 var rule = rules[i].split(':');
4948 result[rule[0].trim()] = rule[1].trim();
4954 getParams: function (attr, value, type) {
4956 var func = 'toggle';
4957 if (typeof attr === 'object') {
4959 func = (value !== undefined) ? value : func;
4961 else if (attr !== undefined && value !== undefined) {
4964 func = (type !== undefined) ? type : func;
4972 setParams: function (node, 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],
4980 $node.attr('data-redactor-style-cache',
4984 else if (key === 'class') {
4985 node = this.inline[params.func + 'Class'](params.data[key],
4991 node = (params.func === 'remove') ? this.inline[params.func + 'Attr'](key,
4993 ) : this.inline[params.func + 'Attr'](key,
4999 if (key === 'style' && node.tagName === 'SPAN') {
5000 $node.attr('data-redactor-span', true);
5009 eachInline: function (node, callback) {
5011 var nodes = (node === undefined) ? this.selection.inlines() : [node];
5013 for (var i = 0; i < nodes.length; i++) {
5014 lastNode = callback(nodes[i])[0];
5022 replaceClass: function (value, node) {
5023 return this.inline.eachInline(node, function (el) {
5024 return $(el).removeAttr('class').addClass(value);
5027 toggleClass: function (value, node) {
5028 return this.inline.eachInline(node, function (el) {
5029 return $(el).toggleClass(value);
5032 addClass: function (value, node) {
5033 return this.inline.eachInline(node, function (el) {
5034 return $(el).addClass(value);
5037 removeClass: function (value, node) {
5038 return this.inline.eachInline(node, function (el) {
5039 return $(el).removeClass(value);
5042 removeAllClass: function (node) {
5043 return this.inline.eachInline(node, function (el) {
5044 return $(el).removeAttr('class');
5049 replaceAttr: function (name, value, node) {
5050 return this.inline.eachInline(node, function (el) {
5051 return $(el).removeAttr(name).attr(name.value);
5054 toggleAttr: function (name, value, node) {
5055 return this.inline.eachInline(node, function (el) {
5056 var attr = $(el).attr(name);
5058 return (attr) ? $(el).removeAttr(name) : $(el).attr(name.value);
5061 addAttr: function (name, value, node) {
5062 return this.inline.eachInline(node, function (el) {
5063 return $(el).attr(name, value);
5066 removeAttr: function (name, node) {
5067 return this.inline.eachInline(node, function (el) {
5070 $el.removeAttr(name);
5071 if (name === 'style') {
5072 $el.removeAttr('data-redactor-style-cache');
5078 removeAllAttr: function (node) {
5079 return this.inline.eachInline(node, function (el) {
5081 var len = el.attributes.length;
5082 for (var z = 0; z < len; z++) {
5083 $el.removeAttr(el.attributes[z].name);
5089 removeSpecificAttr: function (node, key, value) {
5091 if (key === 'style') {
5092 var arr = value.split(':');
5093 var name = arr[0].trim();
5096 if (this.utils.removeEmptyAttr(node, 'style')) {
5097 $el.removeAttr('data-redactor-style-cache');
5101 $el.removeAttr(key)[0];
5106 hasParentStyle: function ($el) {
5107 var $parent = $el.parent();
5109 return ($parent.length === 1 && $parent[0].tagName === $el[0].tagName && $parent.html() === $el[0].outerHTML) ? $parent : false;
5111 addParentStyle: function ($el) {
5112 var $parent = this.inline.hasParentStyle($el);
5114 var style = this.inline.getStyleParams($el.attr('style'));
5116 $parent.attr('data-redactor-style-cache', $parent.attr('style'));
5118 $el.replaceWith(function () {
5119 return $(this).contents();
5123 $el.attr('data-redactor-style-cache', $el.attr('style'));
5128 replaceStyle: function (params, node) {
5129 params = this.inline.getStyleParams(params);
5132 return this.inline.eachInline(node, function (el) {
5134 $el.removeAttr('style').css(params);
5136 var style = $el.attr('style');
5137 if (style) $el.attr('style', style.replace(/"/g
, '\''));
5139 $el
= self
.inline
.addParentStyle($el
);
5144 toggleStyle: function (params
, node
) {
5145 params
= this.inline
.getStyleParams(params
);
5148 return this.inline
.eachInline(node
, function (el
) {
5151 for (var key
in params
) {
5152 var newVal
= params
[key
];
5153 var oldVal
= $el
.css(key
);
5155 oldVal
= (self
.utils
.isRgb(oldVal
)) ? self
.utils
.rgb2hex(oldVal
) : oldVal
.replace(/"/g,
5158 newVal = (self.utils.isRgb(newVal)) ? self.utils.rgb2hex(newVal) : newVal.replace(/"/g
,
5162 if (oldVal
=== newVal
) {
5166 $el
.css(key
, newVal
);
5170 var style
= $el
.attr('style');
5171 if (style
) $el
.attr('style', style
.replace(/"/g, '\''));
5173 if (!self.utils.removeEmptyAttr(el, 'style')) {
5174 $el = self.inline.addParentStyle($el);
5177 $el.removeAttr('data-redactor-style-cache');
5183 addStyle: function (params, node) {
5184 params = this.inline.getStyleParams(params);
5187 return this.inline.eachInline(node, function (el) {
5192 var style = $el.attr('style');
5193 if (style) $el.attr('style', style.replace(/"/g
, '\''));
5195 $el
= self
.inline
.addParentStyle($el
);
5200 removeStyle: function (params
, node
) {
5201 params
= this.inline
.getStyleParams(params
);
5204 return this.inline
.eachInline(node
, function (el
) {
5207 for (var key
in params
) {
5211 if (self
.utils
.removeEmptyAttr(el
, 'style')) {
5212 $el
.removeAttr('data-redactor-style-cache');
5215 $el
.attr('data-redactor-style-cache', $el
.attr('style'));
5221 removeAllStyle: function (node
) {
5222 return this.inline
.eachInline(node
, function (el
) {
5223 return $(el
).removeAttr('style').removeAttr('data-redactor-style-cache');
5226 removeStyleRule: function (name
) {
5227 var parent
= this.selection
.parent();
5228 var nodes
= this.selection
.inlines();
5232 if (parent
&& parent
.tagName
=== 'SPAN') {
5233 this.inline
.removeStyleRuleAttr($(parent
), name
);
5236 for (var i
= 0; i
< nodes
.length
; i
++) {
5239 if ($.inArray(el
.tagName
.toLowerCase(),
5240 this.opts
.inlineTags
5241 ) != -1 && !$el
.hasClass('redactor-selection-marker')) {
5242 this.inline
.removeStyleRuleAttr($el
, name
);
5247 removeStyleRuleAttr: function ($el
, name
) {
5249 if (this.utils
.removeEmptyAttr($el
, 'style')) {
5250 $el
.removeAttr('data-redactor-style-cache');
5253 $el
.attr('data-redactor-style-cache', $el
.attr('style'));
5258 update: function (tag
, attr
, value
, type
) {
5259 tag
= this.inline
.arrangeTag(tag
);
5261 var params
= this.inline
.getParams(attr
, value
, type
);
5262 var nodes
= this.selection
.inlines();
5266 for (var i
= 0; i
< nodes
.length
; i
++) {
5268 if (tag
=== '*' || el
.tagName
.toLowerCase() === tag
) {
5269 result
.push(this.inline
.setParams(el
, params
));
5278 removeFormat: function () {
5279 this.selection
.save();
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();
5290 this.selection
.restore();
5297 insert: function () {
5299 set: function (html
) {
5300 this.code
.set(html
);
5303 html: function (html
, data
) {
5304 this.core
.editor().focus();
5306 var block
= this.selection
.block();
5307 var inline
= this.selection
.inline();
5310 if (typeof data
=== 'undefined') {
5311 data
= this.clean
.getCurrentType(html
, true);
5312 html
= this.clean
.onPaste(html
, data
, true);
5315 html
= $.parseHTML(html
);
5318 var endNode
= $(html
).last();
5320 // delete selected content
5321 var sel
= this.selection
.get();
5322 var range
= this.selection
.range(sel
);
5323 range
.deleteContents();
5325 this.selection
.update(sel
, range
);
5327 // insert list in list
5329 var $list
= $(html
);
5330 if ($list
.length
!== 0 && ($list
[0].tagName
=== 'UL' || $list
[0].tagName
=== 'OL')) {
5332 this.insert
.appendLists(block
, $list
);
5337 if (data
.blocks
&& block
) {
5338 if (this.utils
.isSelectAll()) {
5339 this.core
.editor().html(html
);
5343 var breaked
= this.utils
.breakBlockTag();
5344 if (breaked
=== false) {
5345 this.insert
.placeHtml(html
);
5348 var $last
= $(html
).children().last();
5349 $last
.append(this.marker
.get());
5351 if (breaked
.type
=== 'start') {
5352 breaked
.$block
.before(html
);
5355 breaked
.$block
.after(html
);
5358 this.selection
.restore();
5359 this.core
.editor().find('p').each(function () {
5360 if ($.trim(this.innerHTML
) === '') {
5369 // remove same tag inside
5370 var $div
= $('<div/>').html(html
);
5371 $div
.find(inline
.tagName
.toLowerCase()).each(function () {
5372 $(this).contents().unwrap();
5376 html
= $.parseHTML(html
);
5378 endNode
= $(html
).last();
5382 if (this.utils
.isSelectAll()) {
5383 var $node
= $(this.opts
.emptyHtml
);
5384 this.core
.editor().html('').append($node
);
5386 this.caret
.end($node
);
5389 this.insert
.placeHtml(html
);
5393 this.utils
.disableSelectAll();
5395 if (data
.pre
) this.clean
.cleanPre();
5397 this.caret
.end(endNode
);
5399 text: function (text
) {
5400 text
= text
.toString();
5401 text
= $.trim(text
);
5403 var tmp
= document
.createElement('div');
5404 tmp
.innerHTML
= text
;
5405 text
= tmp
.textContent
|| tmp
.innerText
;
5407 if (typeof text
=== 'undefined') {
5411 this.core
.editor().focus();
5414 var blocks
= this.selection
.blocks();
5417 text
= text
.replace(/\n/g, ' ');
5420 if (this.utils
.isSelectAll()) {
5421 var $node
= $(this.opts
.emptyHtml
);
5422 this.core
.editor().html('').append($node
);
5424 this.caret
.end($node
);
5428 var sel
= this.selection
.get();
5429 var node
= document
.createTextNode(text
);
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);
5438 this.selection
.update(sel
, range
);
5441 // wrap node if selected two or more block tags
5442 if (blocks
.length
> 1) {
5443 $(node
).wrap('<p>');
5444 this.caret
.after(node
);
5448 this.utils
.disableSelectAll();
5449 this.clean
.normalizeCurrentHeading();
5452 raw: function (html
) {
5453 this.core
.editor().focus();
5455 var sel
= this.selection
.get();
5457 var range
= this.selection
.range(sel
);
5458 range
.deleteContents();
5460 var el
= document
.createElement('div');
5461 el
.innerHTML
= html
;
5463 var frag
= document
.createDocumentFragment(), node
, lastNode
;
5464 while ((node
= el
.firstChild
)) {
5465 lastNode
= frag
.appendChild(node
);
5468 range
.insertNode(frag
);
5471 range
= range
.cloneRange();
5472 range
.setStartAfter(lastNode
);
5473 range
.collapse(true);
5474 sel
.removeAllRanges();
5475 sel
.addRange(range
);
5478 node: function (node
, deleteContent
) {
5479 if (typeof this.start
!== 'undefined') {
5480 this.core
.editor().focus();
5483 node
= node
[0] || node
;
5485 var block
= this.selection
.block();
5486 var gap
= this.utils
.isBlockTag(node
.tagName
);
5489 if (this.utils
.isSelectAll()) {
5491 this.core
.editor().html(node
);
5494 this.core
.editor().html($('<p>').html(node
));
5499 else if (gap
&& block
) {
5500 var breaked
= this.utils
.breakBlockTag();
5501 if (breaked
=== false) {
5502 this.insert
.placeNode(node
, deleteContent
);
5505 if (breaked
.type
=== 'start') {
5506 breaked
.$block
.before(node
);
5509 breaked
.$block
.after(node
);
5512 this.core
.editor().find('p:empty').remove();
5516 result
= this.insert
.placeNode(node
, deleteContent
);
5519 this.utils
.disableSelectAll();
5522 this.caret
.end(node
);
5528 appendLists: function (block
, $list
) {
5529 var $block
= $(block
);
5531 var isEmpty
= this.utils
.isEmpty(block
.innerHTML
);
5533 if (isEmpty
|| this.utils
.isEndOfElement(block
)) {
5535 $list
.find('li').each(function () {
5544 else if (this.utils
.isStartOfElement(block
)) {
5545 $list
.find('li').each(function () {
5546 $block
.before(this);
5551 var endOfNode
= this.selection
.extractEndOfNode(block
);
5553 $block
.after($('<li>').append(endOfNode
));
5554 $block
.append($list
);
5558 this.marker
.remove();
5561 this.caret
.end(last
);
5564 placeHtml: function (html
) {
5565 var marker
= document
.createElement('span');
5566 marker
.id
= 'redactor-insert-marker';
5567 marker
= this.insert
.node(marker
);
5569 $(marker
).before(html
);
5570 this.selection
.restore();
5571 this.caret
.after(marker
);
5574 placeNode: function (node
, deleteContent
) {
5575 var sel
= this.selection
.get();
5576 var range
= this.selection
.range(sel
);
5577 if (range
== null) {
5581 if (deleteContent
!== false) {
5582 range
.deleteContents();
5585 range
.insertNode(node
);
5586 range
.collapse(false);
5588 this.selection
.update(sel
, range
);
5590 nodeToPoint: function (e
, node
) {
5591 node
= node
[0] || node
;
5593 if (this.utils
.isEmpty()) {
5594 node
= (this.utils
.isBlock(node
)) ? node
: $('<p />').append(node
);
5596 this.core
.editor().html(node
);
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
);
5611 else if (document
.caretRangeFromPoint
) {
5612 range
= document
.caretRangeFromPoint(x
, y
);
5613 range
.insertNode(node
);
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
);
5629 nodeToCaretPositionFromPoint: function (e
, node
) {
5630 this.insert
.nodeToPoint(e
, node
);
5632 marker: function () {
5633 this.marker
.insert();
5639 keydown: function () {
5641 init: function (e
) {
5642 if (this.rtePaste
) {
5647 var arrow
= (key
>= 37 && key
<= 40);
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();
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');
5661 var keydownStop
= this.core
.callback('keydown', e
);
5662 if (keydownStop
=== false) {
5668 this.shortcuts
.init(e
, key
);
5671 this.keydown
.checkEvents(arrow
, key
);
5672 this.keydown
.setupBuffer(e
, key
);
5674 if (this.utils
.isSelectAll() && (key
=== this.keyCode
.ENTER
|| key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
)) {
5677 this.code
.set(this.opts
.emptyHtml
);
5678 this.events
.changeHandler();
5682 this.keydown
.addArrowsEvent(arrow
);
5683 this.keydown
.setupSelectAll(e
, key
);
5685 // turn off enter key
5686 if (!this.opts
.enterKey
&& key
=== this.keyCode
.ENTER
) {
5690 var sel
= this.selection
.get();
5691 var range
= this.selection
.range(sel
);
5693 if (!range
.collapsed
) {
5694 range
.deleteContents();
5701 if (this.opts
.enterKey
&& key
=== this.keyCode
.DOWN
) {
5702 this.keydown
.onArrowDown();
5706 if (this.opts
.enterKey
&& key
=== this.keyCode
.UP
) {
5707 this.keydown
.onArrowUp();
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();
5716 // on Shift+Space or Ctrl+Space
5717 if (!this.keyup
.lastShiftKey
&& key
=== this.keyCode
.SPACE
&& (e
.ctrlKey
|| e
.shiftKey
)) {
5720 return this.keydown
.onShiftSpace();
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') {
5730 return this.keydown
.onShiftEnter(e
);
5735 if (key
=== this.keyCode
.ENTER
&& !e
.shiftKey
&& !e
.ctrlKey
&& !e
.metaKey
) {
5736 return this.keydown
.onEnter(e
);
5740 if (key
=== this.keyCode
.TAB
|| e
.metaKey
&& key
=== 221 || e
.metaKey
&& key
=== 219) {
5741 return this.keydown
.onTab(e
, key
);
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) {
5751 $prev
.append(this.marker
.get());
5752 $prev
.append($(this.keydown
.block
).html());
5753 $(this.keydown
.block
).remove();
5755 this.selection
.restore();
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) {
5767 var $prev
= this.observe
.image
.closest('figure, p').prev();
5768 this.image
.remove(false);
5769 this.observe
.image
= false;
5771 if ($prev
&& $prev
.length
!== 0) {
5772 this.caret
.end($prev
);
5775 this.core
.editor().focus();
5781 this.keydown
.onBackspaceAndDeleteBefore();
5784 if (key
=== this.keyCode
.DELETE
) {
5785 var $next
= $(this.keydown
.block
).next();
5788 if (this.utils
.isEndOfElement(this.keydown
.block
) && $next
.length
!== 0 && $next
[0].tagName
=== 'FIGURE') {
5793 // append list (safari bug)
5794 var tagLi
= (this.keydown
.block
&& this.keydown
.block
.tagName
=== 'LI') ? this.keydown
.block
: false;
5796 var $list
= $(this.keydown
.block
).parents('ul, ol').last();
5797 var $nextList
= $list
.next();
5799 if (this.utils
.isRedactorParent($list
) && this.utils
.isEndOfElement(
5800 $list
) && $nextList
.length
!== 0 && ($nextList
[0].tagName
=== 'UL' || $nextList
[0].tagName
=== 'OL')) {
5803 $list
.append($nextList
.contents());
5811 if (this.utils
.isEndOfElement(this.keydown
.block
) && $next
.length
!== 0 && $next
[0].tagName
=== 'PRE') {
5812 $(this.keydown
.block
).append($next
.text());
5820 if (key
=== this.keyCode
.DELETE
&& $('#redactor-image-box').length
!== 0) {
5821 this.image
.remove();
5825 if (key
=== this.keyCode
.BACKSPACE
) {
5826 if (this.detect
.isFirefox()) {
5827 this.line
.removeOnBackspace(e
);
5830 // combine list after and before if paragraph is empty
5831 if (this.list
.combineAfterAndBefore(this.keydown
.block
)) {
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();
5844 this.keydown
.removeInvisibleSpace();
5845 this.keydown
.removeEmptyListInTable(e
);
5849 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
5850 this.keydown
.onBackspaceAndDeleteAfter(e
);
5854 onShiftSpace: function () {
5856 this.insert
.raw(' ');
5860 onShiftEnter: function (e
) {
5863 return (this.keydown
.pre
) ? this.keydown
.insertNewLine(e
) : this.insert
.raw(
5866 onBackspaceAndDeleteBefore: function () {
5867 this.utils
.saveScroll();
5869 onBackspaceAndDeleteAfter: function (e
) {
5871 setTimeout($.proxy(function () {
5872 this.code
.syncFire
= false;
5873 this.keydown
.removeEmptyLists();
5876 if (this.opts
.keepStyleAttr
.length
!== 0) {
5877 filter
= ',' + this.opts
.keepStyleAttr
.join(',');
5880 var $styleTags
= this.core
.editor().find('*[style]');
5882 'img, figure, iframe, #redactor-image-box, #redactor-image-editter, [data-redactor-style-cache], [data-redactor-span]' + filter
).removeAttr(
5885 this.keydown
.formatEmpty(e
);
5886 this.code
.syncFire
= true;
5890 onEnter: function (e
) {
5891 var stop
= this.core
.callback('enter', e
);
5892 if (stop
=== false) {
5898 if (this.keydown
.blockquote
&& this.keydown
.exitFromBlockquote(e
) === true) {
5903 if (this.keydown
.pre
) {
5904 return this.keydown
.insertNewLine(e
);
5906 // blockquote & figcaption
5907 else if (this.keydown
.blockquote
|| this.keydown
.figcaption
) {
5908 return this.keydown
.insertBreakLine(e
);
5911 else if (this.keydown
.figure
) {
5912 setTimeout($.proxy(function () {
5913 this.keydown
.replaceToParagraph('FIGURE');
5918 else if (this.keydown
.block
) {
5919 setTimeout($.proxy(function () {
5920 this.keydown
.replaceToParagraph('DIV');
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();
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();
5934 var node
= $(this.opts
.emptyHtml
);
5936 this.caret
.start(node
);
5944 else if (!this.keydown
.block
) {
5945 return this.keydown
.insertParagraph(e
);
5948 // firefox enter into inline element
5949 if (this.detect
.isFirefox() && this.utils
.isInline(this.keydown
.parent
)) {
5950 this.keydown
.insertBreakLine(e
);
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();
5961 //this.caret.start(parent);
5963 var range
= document
.createRange();
5964 range
.setStart(parent
, 0);
5966 var textNode
= document
.createTextNode('\u200B');
5968 range
.insertNode(textNode
);
5969 range
.setStartAfter(textNode
);
5970 range
.collapse(true);
5972 var sel
= window
.getSelection();
5973 sel
.removeAllRanges();
5974 sel
.addRange(range
);
5980 checkEvents: function (arrow
, key
) {
5981 if (!arrow
&& (this.core
.getEvent() === 'click' || this.core
.getEvent() === 'arrow')) {
5982 this.core
.addEvent(false);
5984 if (this.keydown
.checkKeyEvents(key
)) {
5989 checkKeyEvents: function (key
) {
5990 var k
= this.keyCode
;
6003 return ($.inArray(key
, keys
) === -1) ? true : false;
6006 addArrowsEvent: function (arrow
) {
6011 if ((this.core
.getEvent() === 'click' || this.core
.getEvent() === 'arrow')) {
6012 this.core
.addEvent(false);
6016 this.core
.addEvent('arrow');
6018 setupBuffer: function (e
, key
) {
6019 if (this.keydown
.ctrl
&& key
=== 90 && !e
.shiftKey
&& !e
.altKey
&& this.sBuffer
.length
) // z key
6026 else if (this.keydown
.ctrl
&& key
=== 90 && e
.shiftKey
&& !e
.altKey
&& this.sRebuffer
.length
!== 0) {
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
)) {
6037 exitFromBlockquote: function (e
) {
6038 if (!this.utils
.isEndOfElement(this.keydown
.blockquote
)) {
6042 var tmp
= this.clean
.removeSpacesHard($(this.keydown
.blockquote
).html());
6043 if (tmp
.search(/(<br\s?\/?>){1}$/i) !== -1) {
6046 var $last
= $(this.keydown
.blockquote
).children().last();
6048 $last
.filter('br').remove();
6049 $(this.keydown
.blockquote
).children().last().filter('span').remove();
6051 var node
= $(this.opts
.emptyHtml
);
6052 $(this.keydown
.blockquote
).after(node
);
6053 this.caret
.start(node
);
6061 onArrowDown: function () {
6062 var tags
= [this.keydown
.blockquote
, this.keydown
.pre
, this.keydown
.figcaption
];
6064 for (var i
= 0; i
< tags
.length
; i
++) {
6066 this.keydown
.insertAfterLastElement(tags
[i
]);
6071 onArrowUp: function () {
6072 var tags
= [this.keydown
.blockquote
, this.keydown
.pre
, this.keydown
.figcaption
];
6074 for (var i
= 0; i
< tags
.length
; i
++) {
6076 this.keydown
.insertBeforeFirstElement(tags
[i
]);
6081 insertAfterLastElement: function (element
) {
6082 if (!this.utils
.isEndOfElement(element
)) {
6086 var last
= this.core
.editor().contents().last();
6087 var $next
= (element
.tagName
=== 'FIGCAPTION') ? $(this.keydown
.block
).parent().next() : $(
6088 this.keydown
.block
).next();
6090 if ($next
.length
!== 0) {
6093 else if (last
.length
=== 0 && last
[0] !== element
) {
6094 this.caret
.start(last
);
6098 var node
= $(this.opts
.emptyHtml
);
6100 if (element
.tagName
=== 'FIGCAPTION') {
6101 $(element
).parent().after(node
);
6104 $(element
).after(node
);
6107 this.caret
.start(node
);
6111 insertBeforeFirstElement: function (element
) {
6112 if (!this.utils
.isStartOfElement()) {
6116 if (this.core
.editor().contents().length
> 1 && this.core
.editor().contents().first()[0] !== element
) {
6120 var node
= $(this.opts
.emptyHtml
);
6121 $(element
).before(node
);
6122 this.caret
.start(node
);
6125 onTab: function (e
, key
) {
6126 if (!this.opts
.tabKey
) {
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)) {
6138 var isListStart
= (isList
&& this.utils
.isStartOfElement(this.keydown
.block
));
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
);
6146 else if (this.opts
.tabAsSpaces
!== false && !isListStart
) {
6147 node
= document
.createTextNode(Array(this.opts
.tabAsSpaces
+ 1).join(
6149 this.insert
.node(node
);
6152 if (e
.metaKey
&& key
=== 219) {
6153 this.indent
.decrease();
6155 else if (e
.metaKey
&& key
=== 221) {
6156 this.indent
.increase();
6158 else if (!e
.shiftKey
) {
6159 this.indent
.increase();
6162 this.indent
.decrease();
6168 setupSelectAll: function (e
, key
) {
6169 if (this.keydown
.ctrl
&& key
=== 65) {
6170 this.utils
.enableSelectAll();
6172 else if (key
!== this.keyCode
.LEFT_WIN
&& !this.keydown
.ctrl
) {
6173 this.utils
.disableSelectAll();
6176 insertNewLine: function (e
) {
6179 var node
= document
.createTextNode('\n');
6181 var sel
= this.selection
.get();
6182 var range
= this.selection
.range(sel
);
6184 range
.deleteContents();
6185 range
.insertNode(node
);
6187 this.caret
.after(node
);
6191 insertParagraph: function (e
) {
6194 var p
= document
.createElement('p');
6195 //p.innerHTML = this.opts.invisibleSpace;
6196 p
.innerHTML
= '<br>';
6198 var sel
= this.selection
.get();
6199 var range
= this.selection
.range(sel
);
6201 range
.deleteContents();
6202 range
.insertNode(p
);
6204 this.caret
.start(p
);
6208 insertBreakLine: function (e
) {
6209 return this.keydown
.insertBreakLineProcessing(e
);
6211 insertDblBreakLine: function (e
) {
6212 return this.keydown
.insertBreakLineProcessing(e
, true);
6214 insertBreakLineProcessing: function (e
, dbl
) {
6215 e
.stopPropagation();
6217 var br1
= document
.createElement('br');
6218 this.insert
.node(br1
);
6221 var br2
= document
.createElement('br');
6222 this.insert
.node(br2
);
6223 this.caret
.after(br2
);
6226 this.caret
.after(br1
);
6232 wrapToParagraph: function () {
6233 var $current
= $(this.keydown
.current
);
6234 var node
= $('<p>').append($current
.clone());
6235 $current
.replaceWith(node
);
6237 var next
= $(node
).next();
6238 if (typeof (next
[0]) !== 'undefined' && next
[0].tagName
=== 'BR') {
6242 this.caret
.end(node
);
6245 replaceToParagraph: function (tag
) {
6246 var blockElem
= this.selection
.block();
6247 var $prev
= $(blockElem
).prev();
6249 var blockHtml
= blockElem
.innerHTML
.replace(/<br\s?\/?>/gi, '');
6250 if (blockElem
.tagName
=== tag
&& this.utils
.isEmpty(blockHtml
) && !$(blockElem
).hasClass(
6252 var p
= document
.createElement('p');
6253 $(blockElem
).replaceWith(p
);
6255 this.keydown
.setCaretToParagraph(p
);
6259 else if (blockElem
.tagName
=== 'P') {
6260 $(blockElem
).removeAttr('class').removeAttr('style');
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
);
6273 $(blockElem
).off('keyup');
6280 else if ($prev
.hasClass(this.opts
.videoContainerClass
)) {
6281 $prev
.removeAttr('class');
6283 var p
= document
.createElement('p');
6284 $prev
.replaceWith(p
);
6286 this.keydown
.setCaretToParagraph(p
);
6291 setCaretToParagraph: function (p
) {
6292 var range
= document
.createRange();
6293 range
.setStart(p
, 0);
6295 var textNode
= document
.createTextNode('\u200B');
6297 range
.insertNode(textNode
);
6298 range
.setStartAfter(textNode
);
6299 range
.collapse(true);
6301 var sel
= window
.getSelection();
6302 sel
.removeAllRanges();
6303 sel
.addRange(range
);
6305 removeInvisibleSpace: function () {
6306 var $current
= $(this.keydown
.current
);
6307 if ($current
.text().search(/^\u200B$/g) === 0) {
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]);
6316 if (td
.length
!== 0 && $current
.closest('li',
6318 ) && $parent
.children('li').length
=== 1) {
6319 if (!this.utils
.isEmpty($current
.text())) {
6328 this.caret
.start(td
);
6331 removeEmptyLists: function () {
6332 var removeIt = function () {
6333 var html
= $.trim(this.innerHTML
).replace(/\/t\/n/g, '');
6339 this.core
.editor().find('li').each(removeIt
);
6340 this.core
.editor().find('ul, ol').each(removeIt
);
6342 formatEmpty: function (e
) {
6343 var html
= $.trim(this.core
.editor().html());
6345 if (!this.utils
.isEmpty(html
)) {
6351 if (this.opts
.type
=== 'inline' || this.opts
.type
=== 'pre') {
6352 this.core
.editor().html(this.marker
.html());
6353 this.selection
.restore();
6356 var updateHtml = function() {
6357 this.core
.editor().html(this.opts
.emptyHtml
);
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.
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);
6384 keyup: function () {
6386 init: function (e
) {
6387 if (this.rtePaste
) {
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
;
6398 var stop
= this.core
.callback('keyup', e
);
6399 if (stop
=== false) {
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
);
6416 // replace figure to paragraph
6417 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
6418 if (this.utils
.isSelectAll()) {
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
)) {
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');
6434 var $marker
= this.marker
.find();
6435 $('html, body').animate({scrollTop
: $marker
.position().top
+ 20},
6439 this.selection
.restore();
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');
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();
6469 this.opts
.curLang
= this.opts
.langs
[this.opts
.lang
];
6471 get: function (name
) {
6472 return (typeof this.opts
.curLang
[name
] !== 'undefined') ? this.opts
.curLang
[name
] : '';
6480 insert: function () {
6484 this.insert
.html(this.line
.getLineHtml());
6487 var $hr
= this.core
.editor().find('#redactor-hr-tmp-id');
6488 $hr
.removeAttr('id');
6490 this.core
.callback('insertedLine', $hr
);
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>';
6502 removeOnBackspace: function (e
) {
6503 if (!this.utils
.isCollapsed()) {
6507 var $block
= $(this.selection
.block());
6508 if ($block
.length
=== 0 || !this.utils
.isStartOfElement($block
)) {
6512 // if hr is previous element
6513 var $prev
= $block
.prev();
6514 if ($prev
&& $prev
.length
!== 0 && $prev
[0].tagName
=== 'HR') {
6528 return $(this.selection
.inlines('a'));
6531 var nodes
= this.selection
.nodes();
6532 var $link
= $(this.selection
.current()).closest('a', this.core
.editor()[0]);
6534 return ($link
.length
=== 0 || nodes
.length
> 1) ? false : $link
;
6536 unlink: function (e
) {
6537 // if call from clickable element
6538 if (typeof e
!== 'undefined' && e
.preventDefault
) {
6545 var links
= this.selection
.inlines('a');
6546 if (links
.length
=== 0) {
6550 var $links
= this.link
.replaceLinksToText(links
);
6552 this.observe
.closeAllTooltip();
6553 this.core
.callback('deletedLink', $links
);
6556 insert: function (link
, cleaned
) {
6557 var $el
= this.link
.is();
6559 if (cleaned
!== true) {
6560 link
= this.link
.buildLinkFromObject($el
, link
);
6561 if (link
=== false) {
6570 link
= this.core
.callback('beforeInsertingLink', link
);
6572 if ($el
=== false) {
6575 $el
= this.link
.update($el
, link
);
6576 $el
= $(this.insert
.node($el
));
6578 var $parent
= $el
.parent();
6579 if (this.utils
.isRedactorParent($parent
) === false) {
6583 // remove unlink wrapper
6584 if ($parent
.hasClass('redactor-unlink')) {
6585 $parent
.replaceWith(function () {
6586 return $(this).contents();
6590 this.caret
.after($el
);
6591 this.core
.callback('insertedLink', $el
);
6595 $el
= this.link
.update($el
, link
);
6596 this.caret
.after($el
);
6602 update: function ($el
, link
) {
6603 $el
.text(link
.text
);
6604 $el
.attr('href', link
.url
);
6606 this.link
.target($el
, link
.target
);
6611 target: function ($el
, target
) {
6612 return (target
) ? $el
.attr('target', '_blank') : $el
.removeAttr('target');
6614 show: function (e
) {
6615 // if call from clickable element
6616 if (typeof e
!== 'undefined' && e
.preventDefault
) {
6621 this.observe
.closeAllTooltip();
6624 var $el
= this.link
.is();
6627 this.link
.buildModal($el
);
6630 var link
= this.link
.buildLinkFromElement($el
);
6632 // if link cut & paste inside editor browser added self host to a link
6633 link
.url
= this.link
.removeSelfHostFromUrl(link
.url
);
6636 if (this.opts
.linkNewTab
&& !$el
) {
6641 this.link
.setModalValues(link
);
6647 if (this.detect
.isDesktop()) {
6648 $('#redactor-link-url').focus();
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
);
6658 buildModal: function ($el
) {
6659 this.modal
.load('link',
6660 this.lang
.get(($el
=== false) ? 'link-insert' : 'link-edit'),
6665 var $btn
= this.modal
.getActionButton();
6666 $btn
.text(this.lang
.get(($el
=== false) ? 'insert' : 'save')).on('click',
6667 $.proxy(this.link
.callback
, this)
6671 callback: function () {
6673 var link
= this.link
.buildLinkFromModal();
6674 if (link
=== false) {
6682 this.link
.insert(link
, true);
6684 cleanUrl: function (url
) {
6685 return (typeof url
=== 'undefined') ? '' : $.trim(url
.replace(/[^\W\w\D\d+&\'@#/%?=~_
|!:,.;\(\)]/gi
,
6689 cleanText: function (text
) {
6690 return (typeof text
=== 'undefined') ? '' : $.trim(text
.replace(/(<([^>]+)>)/gi,
6694 getText: function (link
) {
6695 return (link
.text
=== '' && link
.url
!== '') ? this.link
.truncateUrl(link
.url
.replace(/<|>/g,
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_]*)?$',
6705 return (reUrl
.test(url
)) ? url
: false;
6707 isMailto: function (url
) {
6708 return (url
.search('@') !== -1 && /(http|ftp|https):\/\//i.test(url
) === false);
6710 isEmpty: function (link
) {
6711 return (link
.url
=== '' || (link
.text
=== '' && link
.url
=== ''));
6713 truncateUrl: function (url
) {
6714 return (url
.length
> this.opts
.linkSize
) ? url
.substring(0,
6718 parse: function (link
) {
6720 if (this.link
.isMailto(link
.url
)) {
6721 link
.url
= 'mailto:' + link
.url
.replace('mailto:', '');
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,
6732 // empty url or text or isn't url
6733 return (this.link
.isEmpty(link
) || link
.url
=== false) ? false : link
;
6736 buildLinkFromModal: function () {
6740 link
.url
= this.link
.cleanUrl($('#redactor-link-url').val());
6743 link
.text
= this.link
.cleanText($('#redactor-link-url-text').val());
6744 link
.text
= this.link
.getText(link
);
6747 link
.target
= ($('#redactor-link-blank').prop('checked')) ? true : false;
6750 return this.link
.parse(link
);
6753 buildLinkFromObject: function ($el
, link
) {
6755 link
.url
= this.link
.cleanUrl(link
.url
);
6758 link
.text
= (typeof link
.text
=== 'undefined' && this.selection
.is()) ? this.selection
.text() : this.link
.cleanText(
6760 link
.text
= this.link
.getText(link
);
6763 link
.target
= ($el
=== false) ? link
.target
: this.link
.buildTarget($el
);
6766 return this.link
.parse(link
);
6769 buildLinkFromElement: function ($el
) {
6772 text
: (this.selection
.is()) ? this.selection
.text() : '',
6776 if ($el
!== false) {
6777 link
.url
= $el
.attr('href');
6778 link
.text
= $el
.text();
6779 link
.target
= this.link
.buildTarget($el
);
6784 buildTarget: function ($el
) {
6785 return (typeof $el
.attr('target') !== 'undefined' && $el
.attr('target') === '_blank') ? true : false;
6787 removeSelfHostFromUrl: function (url
) {
6788 var href
= self
.location
.href
.replace('#', '').replace(/\/$/i, '');
6789 return url
.replace(/^\/\#/, '#').replace(href
, '').replace('mailto:', '');
6791 replaceLinksToText: function (links
) {
6793 var $links
= $.each(links
, function (i
, s
) {
6795 var $unlinked
= $('<span class="redactor-unlink" />').append($el
.contents());
6796 $el
.replaceWith($unlinked
);
6805 // set caret after unlinked node
6806 if (links
.length
=== 1 && this.selection
.isCollapsed()) {
6807 this.caret
.after($first
);
6815 // =linkify -- UNSUPPORTED MODULE
6816 linkify: function () {
6818 isKey: function () {},
6819 isLink: function () {},
6820 isFiltered: function () {},
6821 handler: function () {},
6822 format: function () {},
6823 convertVideoLinks: function () {},
6824 convertImages: function () {},
6825 convertLinks: function () {}
6832 toggle: function (type
) {
6833 if (this.utils
.inBlocks(['table', 'td', 'th', 'tr'])) {
6837 type
= (type
=== 'orderedlist') ? 'ol' : type
;
6838 type
= (type
=== 'unorderedlist') ? 'ul' : type
;
6840 type
= type
.toLowerCase();
6843 this.selection
.save();
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)];
6852 nodes
= (this.list
._isUnformat(type
, nodes
)) ? this.list
._unformat(type
,
6854 ) : this.list
._format(type
, nodes
);
6856 this.selection
.restore();
6861 var current
= this.selection
.current();
6862 var $list
= $(current
).closest('ul, ol', this.core
.editor()[0]);
6864 return ($list
.length
=== 0) ? false : $list
;
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(
6874 this.core
.editor()[0]
6877 if (isEmptyBlock
&& isBlockWrapped
) {
6878 $prev
.children('li').last().append(this.marker
.get());
6879 $prev
.append($next
.contents());
6880 this.selection
.restore();
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'));
6895 if (isFirst
) finalBlocks
.push(blocks
[i
]);
6900 _isUnformat: function (type
, nodes
) {
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') {
6911 return (countLists
=== nodes
.length
);
6913 _uniteBlocks: function (nodes
, tags
) {
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');
6921 if ($cell
.length
!== 0) {
6922 if ($cell
.get(0) !== lastcell
) {
6928 if (this.list
._isUniteBlock(nodes
[i
], tags
)) {
6929 blocks
[z
].push(nodes
[i
]);
6933 if (this.list
._isUniteBlock(nodes
[i
], tags
)) {
6934 blocks
[z
].push(nodes
[i
]);
6943 lastcell
= $cell
.get();
6948 _isUniteBlock: function (node
, tags
) {
6949 return (node
.nodeType
=== 3 || tags
.indexOf(node
.tagName
.toLowerCase()) !== -1);
6951 _createList: function (type
, blocks
, key
) {
6952 var last
= blocks
[blocks
.length
- 1];
6953 var $last
= $(last
);
6954 var $list
= $('<' + type
+ '>');
6959 _createListItem: function (item
) {
6960 var $item
= $('<li>');
6961 if (item
.nodeType
=== 3) {
6966 $item
.append($el
.contents());
6972 _format: function (type
, nodes
) {
6987 var blocks
= this.list
._uniteBlocks(nodes
, tags
);
6990 for (var key
in blocks
) {
6991 var items
= blocks
[key
];
6992 var $list
= this.list
._createList(type
, blocks
[key
]);
6994 for (var i
= 0; i
< items
.length
; i
++) {
6998 if (items
[i
].nodeType
!== 3 && (items
[i
].tagName
=== 'UL' || items
[i
].tagName
=== 'OL')) {
6999 $item
= $(items
[i
]).contents();
7000 $list
.append($item
);
7002 // other blocks or texts
7004 $item
= this.list
._createListItem(items
[i
]);
7005 //this.utils.normalizeTextNodes($item);
7006 $list
.append($item
);
7010 lists
.push($list
.get(0));
7015 _unformat: function (type
, nodes
) {
7017 if (nodes
.length
=== 1) {
7019 var $list
= $(nodes
[0]);
7020 var $items
= $list
.find('li');
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)];
7030 if (selectedItems
.length
=== $items
.length
) {
7031 return this.list
._unformatEntire(nodes
[0]);
7034 var pos
= this.list
._getItemsPosition($items
, selectedItems
);
7037 if (pos
=== 'Top') {
7038 return this.list
._unformatAtSide('before',
7045 else if (pos
=== 'Bottom') {
7046 selectedItems
.reverse();
7047 return this.list
._unformatAtSide('after', selectedItems
, $list
);
7051 else if (pos
=== 'Middle') {
7052 var $last
= $(selectedItems
[selectedItems
.length
- 1]);
7056 var $parent
= false;
7057 var $secondList
= $('<' + $list
.get(0).tagName
.toLowerCase() + '>');
7058 $items
.each(function (i
, node
) {
7060 var $node
= $(node
);
7061 var $childList
= ($node
.children('ul, ol').length
!== 0);
7063 if ($node
.closest('.redactor-split-item').length
=== 0 && ($parent
=== false || $node
.closest(
7064 $parent
).length
=== 0)) {
7065 $node
.addClass('redactor-split-item');
7072 if (node
=== $last
.get(0)) {
7077 $items
.filter('.redactor-split-item').each(function (i
, node
) {
7078 var $node
= $(node
);
7079 $node
.removeClass('redactor-split-item');
7080 $secondList
.append(node
);
7083 $list
.after($secondList
);
7085 selectedItems
.reverse();
7086 for (var i
= 0; i
< selectedItems
.length
; i
++) {
7087 var $item
= $(selectedItems
[i
]);
7088 var $container
= this.list
._createUnformatContainer(
7091 $list
.after($container
);
7092 $container
.find('ul, ol').remove();
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
]);
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
);
7117 $list
.before($container
);
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
);
7128 $list
[type
]($container
);
7130 var $innerLists
= $container
.find('ul, ol').first();
7131 $item
.append($innerLists
);
7133 $innerLists
.each(function (i
, node
) {
7134 var $node
= $(node
);
7135 var $parent
= $node
.closest('li');
7137 if ($parent
.get(0) === selectedItems
[i
]) {
7139 $parent
.addClass('r-unwrapped');
7144 if (this.utils
.isEmpty($item
.html())) $item
.remove();
7148 $list
.find('.r-unwrapped').each(function (node
) {
7149 var $node
= $(node
);
7150 if ($node
.html().trim() === '') {
7154 $node
.removeClass('r-unwrapped');
7158 _getItemsPosition: function ($items
, selectedItems
) {
7161 var sFirst
= selectedItems
[0];
7162 var sLast
= selectedItems
[selectedItems
.length
- 1];
7164 var first
= $items
.first().get(0);
7165 var last
= $items
.last().get(0);
7167 if (first
=== sFirst
&& last
!== sLast
) {
7170 else if (first
!== sFirst
&& last
=== sLast
) {
7176 _createUnformatContainer: function ($item
) {
7177 var $container
= $('<p>');
7178 $container
.append($item
.contents());
7186 marker: function () {
7190 get: function (num
) {
7191 num
= (typeof num
=== 'undefined') ? 1 : num
;
7193 var marker
= document
.createElement('span');
7195 marker
.id
= 'selection-marker-' + num
;
7196 marker
.className
= 'redactor-selection-marker';
7197 marker
.innerHTML
= this.opts
.invisibleSpace
;
7201 html: function (num
) {
7202 return this.utils
.getOuterHtml(this.marker
.get(num
));
7204 find: function (num
) {
7205 num
= (typeof num
=== 'undefined') ? 1 : num
;
7207 return this.core
.editor().find('span#selection-marker-' + num
);
7209 insert: function () {
7210 var sel
= this.selection
.get();
7211 var range
= this.selection
.range(sel
);
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);
7219 remove: function () {
7220 this.core
.editor().find('.redactor-selection-marker').each(this.marker
.iterateRemove
);
7224 insertNode: function (range
, node
, collapse
) {
7225 var parent
= this.selection
.parent();
7226 if (range
=== null || $(parent
).closest('.redactor-in').length
=== 0) {
7230 range
= range
.cloneRange();
7233 range
.collapse(collapse
);
7234 range
.insertNode(node
);
7240 iterateRemove: function (i
, el
) {
7242 var text
= $el
.text().replace(/\u200B/g, '');
7243 var parent
= $el
.parent()[0];
7245 if (text
=== '') $el
.remove(); else $el
.replaceWith(function () { return $(this).contents(); });
7247 // if (parent && parent.normalize) parent.normalize();
7253 modal: function () {
7256 templates: function () {
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>'
7272 $.extend(this.opts
, this.opts
.modal
);
7275 addCallback: function (name
, callback
) {
7276 this.modal
.callbacks
[name
] = callback
;
7278 addTemplate: function (name
, template
) {
7279 this.opts
.modal
[name
] = template
;
7281 getTemplate: function (name
) {
7282 return this.opts
.modal
[name
];
7284 getModal: function () {
7285 return this.$modalBody
;
7287 getActionButton: function () {
7288 return this.$modalBody
.find('#redactor-modal-button-action');
7290 getCancelButton: function () {
7291 return this.$modalBody
.find('#redactor-modal-button-cancel');
7293 getDeleteButton: function () {
7294 return this.$modalBody
.find('#redactor-modal-button-delete');
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
));
7305 this.modal
.getCancelButton().on('mousedown', $.proxy(this.modal
.close
, this));
7307 setDraggable: function () {},
7308 setEnter: function () {},
7309 build: function () {
7310 this.modal
.buildOverlay();
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('×');
7318 this.$modalBody
= $('<div id="redactor-modal-body" />');
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
);
7327 buildOverlay: function () {
7328 this.$modalOverlay
= $('<div id="redactor-modal-overlay">').hide();
7329 $('body').prepend(this.$modalOverlay
);
7331 enableEvents: function () {},
7332 disableEvents: function () {},
7333 closeHandler: function () {},
7334 close: function () { /* WoltLabModal.js */ }
7339 observe: function () {
7342 if (typeof this.opts
.destroyed
!== 'undefined') {
7346 this.observe
.links();
7347 this.observe
.images();
7350 isCurrent: function ($el
, $current
) {
7351 if (typeof $current
=== 'undefined') {
7352 $current
= $(this.selection
.current());
7355 return $current
.is($el
) || $current
.parents($el
).length
> 0;
7357 toolbar: function () {
7358 this.observe
.buttons();
7359 this.observe
.dropdowns();
7361 buttons: function (e
, btnName
) {
7362 var current
= this.selection
.current();
7363 var parent
= this.selection
.parent();
7366 this.button
.setInactiveAll();
7369 this.button
.setInactiveAll(btnName
);
7372 if (e
=== false && btnName
!== 'html') {
7373 if ($.inArray(btnName
, this.opts
.activeButtons
) !== -1) {
7374 this.button
.toggleActive(btnName
);
7379 if (!this.utils
.isRedactorParent(current
)) {
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');
7390 this.button
.enable('horizontalrule');
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]);
7398 if (parentEl
.length
!== 0 && !this.utils
.isRedactorParent(parentEl
)) {
7402 if (!this.utils
.isRedactorParent(currentEl
)) {
7406 if (parentEl
.length
!== 0 || currentEl
.closest(key
,
7409 this.button
.setActive(value
);
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
);
7420 $.each(this.opts
.observe
.dropdowns
, $.proxy(function (key
, value
) {
7421 var observe
= value
.observe
, element
= observe
.element
,
7423 inValues
= typeof observe
['in'] !== 'undefined' ? observe
['in'] : false,
7424 outValues
= typeof observe
.out
!== 'undefined' ? observe
.out
: false;
7426 if (($current
.closest(element
).length
> 0 && isRedactor
) || (element
=== 'a' && finded
!== 0)) {
7427 this.observe
.setDropdownProperties($item
, inValues
, outValues
);
7430 this.observe
.setDropdownProperties($item
, outValues
, inValues
);
7435 setDropdownProperties: function ($item
, addProperties
, deleteProperties
) {
7436 if (deleteProperties
&& typeof deleteProperties
.attr
!== 'undefined') {
7437 this.observe
.setDropdownAttr($item
, deleteProperties
.attr
, true);
7440 if (typeof addProperties
.attr
!== 'undefined') {
7441 this.observe
.setDropdownAttr($item
, addProperties
.attr
);
7444 if (typeof addProperties
.title
!== 'undefined') {
7445 $item
.find('span').text(addProperties
.title
);
7448 setDropdownAttr: function ($item
, properties
, isDelete
) {
7449 $.each(properties
, function (key
, value
) {
7450 if (key
=== 'class') {
7452 $item
.addClass(value
);
7455 $item
.removeClass(value
);
7460 $item
.attr(key
, value
);
7463 $item
.removeAttr(key
);
7468 addDropdown: function ($item
, btnName
, btnObject
) {
7469 if (typeof btnObject
.observe
=== 'undefined') {
7473 btnObject
.item
= $item
;
7475 this.opts
.observe
.dropdowns
.push(btnObject
);
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
) {
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(); }
7488 this.image
.setEditable($img
);
7493 links: function () {
7494 if (this.opts
.linkTooltip
) {
7495 this.core
.editor().find('a').each($.proxy(function (i
, s
) {
7497 if ($link
.data('cached') !== true) {
7498 $link
.data('cached', true);
7500 'touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7501 $.proxy(this.observe
.showTooltip
, this)
7508 getTooltipPosition: function ($link
) {
7509 return $link
.offset();
7511 showTooltip: function (e
) {
7512 var $el
= $(e
.target
);
7514 if ($el
[0].tagName
=== 'IMG') {
7518 if ($el
[0].tagName
!== 'A') {
7519 $el
= $el
.closest('a', this.$editor
[0]);
7522 if ($el
[0].tagName
!== 'A') {
7528 var pos
= this.observe
.getTooltipPosition($link
);
7529 var tooltip
= $('<span class="redactor-link-tooltip"></span>');
7531 var href
= $link
.attr('href');
7532 if (href
=== undefined) {
7536 if (href
.length
> 24) {
7537 href
= href
.substring(0, 24) + '...';
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');
7549 tooltip
.append(aLink
).append(' | ').append(aEdit
).append(' | ').append(aUnlink
);
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
;
7557 left
: pos
.left
+ 'px'
7560 $('.redactor-link-tooltip').remove();
7561 $('body').append(tooltip
);
7563 this.core
.editor().on('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7564 $.proxy(this.observe
.closeTooltip
, this)
7566 $(document
).on('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7567 $.proxy(this.observe
.closeTooltip
, this)
7570 closeAllTooltip: function () {
7571 $('.redactor-link-tooltip').remove();
7573 closeTooltip: function (e
) {
7574 e
= e
.originalEvent
|| e
;
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') {
7581 else if ((target
.tagName
=== 'A' && this.utils
.isRedactorParent(target
)) || $(
7582 target
).hasClass('redactor-link-tooltip-action')) {
7586 this.observe
.closeAllTooltip();
7588 this.core
.editor().off('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7589 $.proxy(this.observe
.closeTooltip
, this)
7591 $(document
).off('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7592 $.proxy(this.observe
.closeTooltip
, this)
7600 offset: function () {
7602 get: function (node
) {
7603 var cloned
= this.offset
.clone(node
);
7604 if (cloned
=== false) {
7608 var div
= document
.createElement('div');
7609 div
.appendChild(cloned
.cloneContents());
7610 div
.innerHTML
= div
.innerHTML
.replace(/<img(.*?[^>])>$/gi, 'i');
7612 var text
= $.trim($(div
).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
7619 clone: function (node
) {
7620 var sel
= this.selection
.get();
7621 var range
= this.selection
.range(sel
);
7623 if (range
=== null && typeof node
=== 'undefined') {
7627 node
= (typeof node
=== 'undefined') ? this.$editor
: node
;
7628 if (node
=== false) {
7632 node
= node
[0] || node
;
7634 var cloned
= range
.cloneRange();
7635 cloned
.selectNodeContents(node
);
7636 cloned
.setEnd(range
.endContainer
, range
.endOffset
);
7640 set: function (start
, end
) {
7641 end
= (typeof end
=== 'undefined') ? start
: end
;
7643 if (!this.focus
.is()) {
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
,
7656 while ((node
= walker
.nextNode()) !== null) {
7657 offset
+= node
.nodeValue
.length
;
7658 if (offset
> start
) {
7659 range
.setStart(node
, node
.nodeValue
.length
+ start
- offset
);
7663 if (offset
>= end
) {
7664 range
.setEnd(node
, node
.nodeValue
.length
+ end
- offset
);
7669 range
.collapse(false);
7670 this.selection
.update(sel
, range
);
7676 paragraphize: function () {
7678 load: function (html
) {
7679 if (this.opts
.paragraphize
=== false || this.opts
.type
=== 'inline' || this.opts
.type
=== 'pre') {
7683 if (html
=== '' || html
=== '<p></p>') {
7684 return this.opts
.emptyHtml
;
7689 this.paragraphize
.safes
= [];
7690 this.paragraphize
.z
= 0;
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>');
7697 html
= this.paragraphize
.getSafes(html
);
7699 html
= html
.replace('<br>', '\n');
7700 html
= this.paragraphize
.convert(html
);
7702 html
= this.paragraphize
.clear(html
);
7703 html
= this.paragraphize
.restoreSafes(html
);
7706 html
= html
.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts
.paragraphizeBlocks
.join(
7707 '|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');
7709 return $.trim(html
);
7711 getSafes: function (html
) {
7712 var $div
= $('<div />').append(html
);
7714 // remove paragraphs in blockquotes
7715 $div
.find('blockquote p').replaceWith(function () {
7716 return $(this).append('<br />').contents();
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
;
7723 return $(s
).replaceWith('\n#####replace' + this.paragraphize
.z
+ '#####\n\n');
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
;
7732 return $(s
).replaceWith('\n#####replace' + this.paragraphize
.z
+ '#####\n\n');
7737 restoreSafes: function (html
) {
7738 $.each(this.paragraphize
.safes
, function (i
, s
) {
7739 s
= (typeof s
!== 'undefined') ? s
.replace(/\$/g, '$') : s
;
7740 html
= html
.replace('#####replace' + i
+ '#####', s
);
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');
7752 html
= html
.replace(re1
, ' ');
7753 html
= $.trim(html
);
7755 var re2
= /xparagraphmarkerzxparagraphmarkerz/gi;
7756 html
= html
.replace(re2
, '</p><p>');
7758 var re3
= /xparagraphmarkerz/gi;
7759 html
= html
.replace(re3
, '<br>');
7761 html
= '<p>' + html
+ '</p>';
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> <\/p>/gi, '');
7771 html
= html
.replace(/<p>\s?<br> <\/p>/gi, '');
7772 html
= html
.replace(/<p>\s?<br>/gi, '<p>');
7776 clear: function (html
) {
7778 html
= html
.replace(
7779 /<p>(.*?)#####replace(.*?)#####\s?<\/p>/gi,
7780 '<p>$1</p>#####replace$2#####'
7782 html
= html
.replace(/(<br\s?\/?>){2,}<\/p>/gi, '</p>');
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>');
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'), '');
7803 paste: function () {
7805 init: function (e
) {
7806 this.rtePaste
= true;
7807 var pre
= (this.opts
.type
=== 'pre' || this.utils
.isCurrentOrParent('pre')) ? true : false;
7810 if (this.detect
.isDesktop()) {
7812 if (!this.paste
.pre
&& this.opts
.clipboardImageUpload
&& this.opts
.imageUpload
&& this.paste
.detectClipboardUpload(
7814 if (this.detect
.isIe()) {
7815 setTimeout($.proxy(this.paste
.clipboardUpload
, this),
7824 this.utils
.saveScroll();
7825 this.selection
.save();
7826 this.paste
.createPasteBox(pre
);
7828 $(window
).on('scroll.redactor-freeze', $.proxy(function () {
7829 $(window
).scrollTop(this.saveBodyScroll
);
7833 setTimeout($.proxy(function () {
7834 var html
= this.paste
.getPasteBoxCode(pre
);
7838 this.selection
.restore();
7840 this.utils
.restoreScroll();
7843 var data
= this.clean
.getCurrentType(html
);
7846 html
= this.clean
.onPaste(html
, data
);
7849 var returned
= this.core
.callback('paste', html
);
7850 html
= (typeof returned
=== 'undefined') ? html
: returned
;
7852 this.paste
.insert(html
, data
);
7853 this.rtePaste
= false;
7855 // clean pre breaklines
7857 this.clean
.cleanPre();
7860 $(window
).off('scroll.redactor-freeze');
7865 getPasteBoxCode: function (pre
) {
7866 var html
= (pre
) ? this.$pasteBox
.val() : this.$pasteBox
.html();
7867 this.$pasteBox
.remove();
7871 createPasteBox: function (pre
) {
7879 this.$pasteBox
= (pre
) ? $('<textarea>').css(css
) : $('<div>').attr('contenteditable',
7882 this.paste
.appendPasteBox();
7883 this.$pasteBox
.focus();
7885 appendPasteBox: function () {
7886 if (this.detect
.isIe()) {
7887 this.core
.box().append(this.$pasteBox
);
7891 var $visibleModals
= $('.modal-body:visible');
7892 if ($visibleModals
.length
> 0) {
7893 $visibleModals
.append(this.$pasteBox
);
7896 $('body').prepend(this.$pasteBox
);
7900 detectClipboardUpload: function (e
) {
7901 e
= e
.originalEvent
|| e
;
7903 var clipboard
= e
.clipboardData
;
7904 if (this.detect
.isIe() || this.detect
.isFirefox()) {
7908 // prevent safari fake url
7909 var types
= clipboard
.types
;
7910 if (types
.indexOf('public.tiff') !== -1) {
7915 if (!clipboard
.items
|| !clipboard
.items
.length
) {
7919 var file
= clipboard
.items
[0].getAsFile();
7920 if (file
=== null) {
7924 var reader
= new FileReader();
7925 reader
.readAsDataURL(file
);
7926 reader
.onload
= $.proxy(this.paste
.insertFromClipboard
, this);
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) {
7937 var formData
= !!window
.FormData
? new FormData() : null;
7938 if (!window
.FormData
) {
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 />'));
7951 var $parent
= $(s
).parent();
7952 this.utils
.replaceToTag($parent
, 'figure');
7956 this.core
.callback('imageUpload', $(s
), data
);
7960 var blob
= this.utils
.dataURItoBlob(s
.src
);
7962 formData
.append('clipboard', 1);
7963 formData
.append(this.opts
.imageUploadParam
, blob
);
7965 this.upload
.send(formData
, false);
7967 this.rtePaste
= false;
7971 insertFromClipboard: function (e
) {
7972 var formData
= !!window
.FormData
? new FormData() : null;
7973 if (!window
.FormData
) {
7977 this.upload
.direct
= true;
7978 this.upload
.type
= 'image';
7979 this.upload
.url
= this.opts
.imageUpload
;
7980 this.upload
.callback
= this.image
.insert
;
7982 var blob
= this.utils
.dataURItoBlob(e
.target
.result
);
7984 formData
.append('clipboard', 1);
7985 formData
.append(this.opts
.imageUploadParam
, blob
);
7987 this.upload
.send(formData
, e
);
7988 this.rtePaste
= false;
7990 insert: function (html
, data
) {
7992 this.insert
.raw(html
);
7994 else if (data
.text
) {
7995 this.insert
.text(html
);
7998 this.insert
.html(html
, data
);
8001 // Firefox Clipboard Observe
8002 if (this.detect
.isFirefox() && this.opts
.imageUpload
&& this.opts
.clipboardImageUpload
) {
8003 setTimeout($.proxy(this.paste
.clipboardUpload
, this), 100);
8010 // =placeholder -- UNSUPPORTED MODULE
8011 placeholder: function () {
8013 enable: function () {},
8014 show: function () {},
8015 update: function () {},
8016 hide: 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 () {}
8031 // =progress -- UNSUPPORTED MODULE
8032 progress: function () {
8036 target
: document
.body
, // or id selector
8037 show: function () {},
8038 hide: function () {},
8039 update: function () {},
8041 build: function () {},
8042 destroy: function () {}
8047 selection: function () {
8050 if (window
.getSelection
) {
8051 return window
.getSelection();
8053 else if (document
.selection
&& document
.selection
.type
!== 'Control') {
8054 return document
.selection
;
8059 range: function (sel
) {
8060 if (typeof sel
=== 'undefined') {
8061 sel
= this.selection
.get();
8064 if (sel
.getRangeAt
&& sel
.rangeCount
) {
8065 return sel
.getRangeAt(0);
8071 return (this.selection
.isCollapsed()) ? false : true;
8073 isRedactor: function () {
8074 var range
= this.selection
.range();
8076 if (range
!== null) {
8077 var el
= range
.startContainer
.parentNode
;
8079 if ($(el
).hasClass('redactor-in') || $(el
).parents('.redactor-in').length
!== 0) {
8086 isCollapsed: function () {
8087 var sel
= this.selection
.get();
8089 return (sel
=== null) ? false : sel
.isCollapsed
;
8091 update: function (sel
, range
) {
8092 if (range
=== null) {
8096 sel
.removeAllRanges();
8097 sel
.addRange(range
);
8099 current: function () {
8100 var sel
= this.selection
.get();
8102 return (sel
=== null) ? false : sel
.anchorNode
;
8104 parent: function () {
8105 var current
= this.selection
.current();
8107 return (current
=== null) ? false : current
.parentNode
;
8109 block: function (node
) {
8110 node
= node
|| this.selection
.current();
8113 if (this.utils
.isBlockTag(node
.tagName
)) {
8114 return ($(node
).hasClass('redactor-in')) ? false : node
;
8117 node
= node
.parentNode
;
8122 inline: function (node
) {
8123 node
= node
|| this.selection
.current();
8126 if (this.utils
.isInlineTag(node
.tagName
)) {
8127 return ($(node
).hasClass('redactor-in')) ? false : node
;
8130 node
= node
.parentNode
;
8135 element: function (node
) {
8137 node
= this.selection
.current();
8141 if (node
.nodeType
=== 1) {
8142 if ($(node
).hasClass('redactor-in')) {
8149 node
= node
.parentNode
;
8155 var current
= this.selection
.current();
8157 return (current
=== null) ? false : this.selection
.current().previousSibling
;
8160 var current
= this.selection
.current();
8162 return (current
=== null) ? false : this.selection
.current().nextSibling
;
8164 blocks: function (tag
) {
8166 var nodes
= this.selection
.nodes(tag
);
8168 $.each(nodes
, $.proxy(function (i
, node
) {
8169 if (this.utils
.isBlock(node
)) {
8175 var block
= this.selection
.block();
8176 if (blocks
.length
=== 0 && block
=== false) {
8179 else if (blocks
.length
=== 0 && block
!== false) {
8187 inlines: function (tag
) {
8189 var nodes
= this.selection
.nodes(tag
);
8191 $.each(nodes
, $.proxy(function (i
, node
) {
8192 if (this.utils
.isInline(node
)) {
8198 var inline
= this.selection
.inline();
8199 if (inlines
.length
=== 0 && inline
=== false) {
8202 else if (inlines
.length
=== 0 && inline
!== false) {
8209 nodes: function (tag
) {
8210 var filter
= (typeof tag
=== 'undefined') ? [] : (($.isArray(tag
)) ? tag
: [tag
]);
8212 var sel
= this.selection
.get();
8213 var range
= this.selection
.range(sel
);
8215 var resultNodes
= [];
8217 if (this.utils
.isCollapsed()) {
8218 nodes
= [this.selection
.current()];
8221 var node
= range
.startContainer
;
8222 var endNode
= range
.endContainer
;
8225 if (node
=== endNode
) {
8230 while (node
&& node
!== endNode
) {
8231 nodes
.push(node
= this.selection
.nextNode(node
));
8234 // partially selected nodes
8235 node
= range
.startContainer
;
8236 while (node
&& node
!== range
.commonAncestorContainer
) {
8237 nodes
.unshift(node
);
8238 node
= node
.parentNode
;
8242 // remove service nodes
8243 $.each(nodes
, function (i
, s
) {
8245 var tagName
= (s
.nodeType
!== 1) ? false : s
.tagName
.toLowerCase();
8247 if ($(s
).hasClass('redactor-script-tag') || $(s
).hasClass(
8248 'redactor-selection-marker')) {
8251 else if (tagName
&& filter
.length
!== 0 && $.inArray(tagName
,
8257 resultNodes
.push(s
);
8262 return (resultNodes
.length
=== 0) ? [] : resultNodes
;
8264 nextNode: function (node
) {
8265 if (node
.hasChildNodes()) {
8266 return node
.firstChild
;
8269 while (node
&& !node
.nextSibling
) {
8270 node
= node
.parentNode
;
8277 return node
.nextSibling
;
8281 this.marker
.insert();
8282 this.savedSel
= this.core
.editor().html();
8284 restore: function (removeMarkers
) {
8285 var node1
= this.marker
.find(1);
8286 var node2
= this.marker
.find(2);
8288 if (this.detect
.isFirefox()) {
8289 this.core
.editor().focus();
8292 if (node1
.length
!== 0 && node2
.length
!== 0) {
8293 this.caret
.set(node1
, node2
);
8295 else if (node1
.length
!== 0) {
8296 this.caret
.start(node1
);
8299 this.core
.editor().focus();
8302 if (removeMarkers
!== false) {
8303 this.marker
.remove();
8304 this.savedSel
= false;
8307 saveInstant: function () {
8308 var el
= this.core
.editor()[0];
8309 var doc
= el
.ownerDocument
, win
= doc
.defaultView
;
8310 var sel
= win
.getSelection();
8312 if (!sel
.getRangeAt
|| !sel
.rangeCount
) {
8316 var range
= sel
.getRangeAt(0);
8317 var selectionRange
= range
.cloneRange();
8319 selectionRange
.selectNodeContents(el
);
8320 selectionRange
.setEnd(range
.startContainer
, range
.startOffset
);
8322 var start
= selectionRange
.toString().length
;
8326 end
: start
+ range
.toString().length
,
8327 node
: range
.startContainer
8332 restoreInstant: function (saved
) {
8333 if (typeof saved
=== 'undefined' && !this.saved
) {
8337 this.saved
= (typeof saved
!== 'undefined') ? saved
: this.saved
;
8339 var $node
= this.core
.editor().find(this.saved
.node
);
8340 if ($node
.length
!== 0 && $node
.text().trim().replace(/\u200B/g,
8344 var range
= document
.createRange();
8345 range
.setStart($node
[0], 0);
8347 var sel
= window
.getSelection();
8348 sel
.removeAllRanges();
8349 sel
.addRange(range
);
8356 var el
= this.core
.editor()[0];
8357 var doc
= el
.ownerDocument
, win
= doc
.defaultView
;
8358 var charIndex
= 0, range
= doc
.createRange();
8360 range
.setStart(el
, 0);
8361 range
.collapse(true);
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
);
8372 if (foundStart
&& this.saved
.end
>= charIndex
&& this.saved
.end
<= nextCharIndex
) {
8373 range
.setEnd(node
, this.saved
.end
- charIndex
);
8376 charIndex
= nextCharIndex
;
8379 var i
= node
.childNodes
.length
;
8381 nodeStack
.push(node
.childNodes
[i
]);
8386 var sel
= win
.getSelection();
8387 sel
.removeAllRanges();
8388 sel
.addRange(range
);
8390 node: function (node
) {
8391 $(node
).prepend(this.marker
.get(1));
8392 $(node
).append(this.marker
.get(2));
8394 this.selection
.restore();
8397 this.core
.editor().focus();
8399 var sel
= this.selection
.get();
8400 var range
= this.selection
.range(sel
);
8402 range
.selectNodeContents(this.core
.editor()[0]);
8404 this.selection
.update(sel
, range
);
8406 remove: function () {
8407 this.selection
.get().removeAllRanges();
8409 replace: function (html
) {
8410 this.insert
.html(html
);
8413 return this.selection
.get().toString();
8417 var sel
= this.selection
.get();
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());
8426 html
= this.clean
.onGet(container
.innerHTML
);
8431 extractEndOfNode: function (node
) {
8432 var sel
= this.selection
.get();
8433 var range
= this.selection
.range(sel
);
8435 var clonedRange
= range
.cloneRange();
8436 clonedRange
.selectNodeContents(node
);
8437 clonedRange
.setStart(range
.endContainer
, range
.endOffset
);
8439 return clonedRange
.extractContents();
8443 removeMarkers: function () {
8444 this.marker
.remove();
8446 marker: function (num
) {
8447 return this.marker
.get(num
);
8449 markerHtml: function (num
) {
8450 return this.marker
.html(num
);
8457 shortcuts: function () {
8459 // based on https://github.com/jeresig/jquery.hotkeys
8460 hotkeysSpecialKeys
: {
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)) {
8558 $.each(this.opts
.shortcuts
, $.proxy(function (str
, command
) {
8559 this.shortcuts
.build(e
, str
, command
);
8564 build: function (e
, str
, command
) {
8565 var handler
= $.proxy(function () {
8566 this.shortcuts
.buildHandler(command
);
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
);
8579 buildHandler: function (command
) {
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
);
8588 this[command
.func
].apply(this, command
.params
);
8591 handler: function (e
, keys
, origHandler
) {
8592 keys
= keys
.toLowerCase().split(' ');
8594 var special
= this.shortcuts
.hotkeysSpecialKeys
[e
.keyCode
];
8595 var character
= String
.fromCharCode(e
.which
).toLowerCase();
8596 var modif
= '', possible
= {};
8598 $.each(['alt', 'ctrl', 'meta', 'shift'], function (index
, specialKey
) {
8599 if (e
[specialKey
+ 'Key'] && special
!== specialKey
) {
8600 modif
+= specialKey
+ '+';
8605 possible
[modif
+ special
] = true;
8609 possible
[modif
+ character
] = true;
8610 possible
[modif
+ this.shortcuts
.hotkeysShiftNums
[character
]] = true;
8612 // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
8613 if (modif
=== 'shift+') {
8614 possible
[this.shortcuts
.hotkeysShiftNums
[character
]] = true;
8618 var len
= keys
.length
;
8619 for (var i
= 0; i
< len
; i
++) {
8620 if (possible
[keys
[i
]]) {
8622 return origHandler
.apply(this, arguments
);
8629 // =storage -- UNSUPPORTED MODULE
8630 storage: function () {
8633 add: function () {},
8634 status: function () {},
8635 observe: function () {},
8636 changes: function () {}
8642 toolbar: function () {
8644 build: function () {
8645 this.button
.hideButtons();
8646 this.button
.hideButtonsOnMobile();
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
);
8653 this.button
.$toolbar
= this.$toolbar
;
8654 this.button
.setFormatting();
8655 this.button
.load(this.$toolbar
);
8657 require(['Core'], (function(Core
) {
8658 this.$toolbar
[0].addEventListener('keydown', this.toolbar
.keydown
.bind(this, Core
));
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 () {},
8675 * @param {object} Core
8676 * @param {KeyboardEvent} event
8678 keydown: function(Core
, event
) {
8679 var activeButton
= document
.activeElement
;
8680 if (!activeButton
.classList
.contains('re-button')) {
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) {
8691 // [Enter] || [Space]
8692 if (event
.which
=== 13 || event
.which
=== 32) {
8693 event
.preventDefault();
8695 require(['Core'], function(Core
) {
8696 Core
.triggerEvent(activeButton
, 'mousedown');
8702 // [ArrowDown] opens drop-down menus, but does nothing on "regular" buttons.
8703 if (event
.which
=== 40) {
8704 if (elAttr(activeButton
, 'aria-haspopup') !== 'true') {
8708 event
.preventDefault();
8709 Core
.triggerEvent(activeButton
, 'mousedown');
8711 var dropdown
= $(activeButton
).data('dropdown');
8712 var firstItem
= elBySel('li', dropdown
[0]);
8713 if (firstItem
) firstItem
.focus();
8717 event
.preventDefault();
8719 var buttons
= Array
.prototype.slice
.call(elBySelAll('.re-button', this.$toolbar
[0]));
8720 var newActiveButton
= null;
8722 if (event
.which
=== 35) {
8723 newActiveButton
= buttons
[buttons
.length
- 1];
8726 else if (event
.which
=== 36) {
8727 newActiveButton
= buttons
[0];
8730 var index
= buttons
.indexOf(activeButton
);
8733 if (event
.which
=== 37) {
8737 index
= buttons
.length
- 1;
8741 else if (event
.which
=== 39) {
8744 if (index
=== buttons
.length
) {
8749 newActiveButton
= buttons
[index
];
8752 if (newActiveButton
!== null) {
8753 newActiveButton
.focus();
8759 // =upload -- UNSUPPORTED MODULE
8760 upload: function () {
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 () {}
8781 // =s3 -- UNSUPPORTED MODULE
8782 uploads3: function () {
8784 send: function () {},
8785 executeOnSignedUrl: function () {},
8786 createCORSRequest: function () {},
8787 sendToS3: function () {}
8792 utils: function () {
8794 isEmpty: function (html
) {
8795 html
= (typeof html
=== 'undefined') ? this.core
.editor().html() : html
;
8797 html
= html
.replace(/[\u200B-\u200D\uFEFF]/g, '');
8798 html
= html
.replace(/ /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');
8805 // remove empty tags
8806 html
= html
.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8807 html
= html
.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8809 html
= $.trim(html
);
8813 isElement: function (obj
) {
8815 // Using W3 DOM2 (works for FF, Opera and Chrome)
8816 return obj
instanceof HTMLElement
;
8819 return (typeof obj
=== 'object') && (obj
.nodeType
=== 1) && (typeof obj
.style
=== 'object') && (typeof obj
.ownerDocument
=== 'object');
8822 strpos: function (haystack
, needle
, offset
) {
8823 var i
= haystack
.indexOf(needle
, offset
);
8824 return i
>= 0 ? i
: false;
8826 dataURItoBlob: function (dataURI
) {
8828 if (dataURI
.split(',')[0].indexOf('base64') >= 0) {
8829 byteString
= atob(dataURI
.split(',')[1]);
8832 byteString
= unescape(dataURI
.split(',')[1]);
8835 var mimeString
= dataURI
.split(',')[0].split(':')[1].split(';')[0];
8837 var ia
= new Uint8Array(byteString
.length
);
8838 for (var i
= 0; i
< byteString
.length
; i
++) {
8839 ia
[i
] = byteString
.charCodeAt(i
);
8842 return new Blob([ia
], {type
: mimeString
});
8844 getOuterHtml: function (el
) {
8845 return $('<div>').append($(el
).eq(0).clone()).html();
8847 cloneAttributes: function (from, to
) {
8848 from = from[0] || from;
8851 var attrs
= from.attributes
;
8852 var len
= attrs
.length
;
8854 var attr
= attrs
[len
];
8855 to
.attr(attr
.name
, attr
.value
);
8860 breakBlockTag: function () {
8861 var block
= this.selection
.block();
8866 var isEmpty
= this.utils
.isEmpty(block
.innerHTML
);
8868 var tag
= block
.tagName
.toLowerCase();
8869 if (tag
=== 'pre' || tag
=== 'li' || tag
=== 'td' || tag
=== 'th') {
8873 if (!isEmpty
&& this.utils
.isStartOfElement(block
)) {
8876 $next
: $(block
).next(),
8880 else if (!isEmpty
&& this.utils
.isEndOfElement(block
)) {
8883 $next
: $(block
).next(),
8888 var endOfNode
= this.selection
.extractEndOfNode(block
);
8889 var $nextPart
= $('<' + tag
+ ' />').append(endOfNode
);
8891 $nextPart
= this.utils
.cloneAttributes(block
, $nextPart
);
8892 $(block
).after($nextPart
);
8901 inBlocks: function (tags
) {
8902 tags
= ($.isArray(tags
)) ? tags
: [tags
];
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();
8911 if ($.inArray(tag
, tags
) !== -1) {
8920 inInlines: function (tags
) {
8921 tags
= ($.isArray(tags
)) ? tags
: [tags
];
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();
8929 if ($.inArray(tag
, tags
) !== -1) {
8937 isTag: function (current
, tag
) {
8938 var element
= $(current
).closest(tag
, this.core
.editor()[0]);
8939 if (element
.length
=== 1) {
8945 isBlock: function (block
) {
8946 if (block
=== null) {
8950 block
= block
[0] || block
;
8952 return block
&& this.utils
.isBlockTag(block
.tagName
);
8954 isBlockTag: function (tag
) {
8955 return (typeof tag
=== 'undefined') ? false : this.reIsBlock
.test(tag
);
8957 isInline: function (inline
) {
8958 inline
= inline
[0] || inline
;
8960 return inline
&& this.utils
.isInlineTag(inline
.tagName
);
8962 isInlineTag: function (tag
) {
8963 return (typeof tag
=== 'undefined') ? false : this.reIsInline
.test(tag
);
8964 }, // parents detection
8965 isRedactorParent: function (el
) {
8970 if ($(el
).parents('.redactor-in').length
=== 0 || $(el
).hasClass('redactor-in')) {
8976 isCurrentOrParentHeader: function () {
8977 return this.utils
.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
8979 isCurrentOrParent: function (tagName
) {
8980 var parent
= this.selection
.parent();
8981 var current
= this.selection
.current();
8983 if ($.isArray(tagName
)) {
8985 $.each(tagName
, $.proxy(function (i
, s
) {
8986 if (this.utils
.isCurrentOrParentOne(current
, parent
, s
)) {
8991 return (matched
=== 0) ? false : true;
8994 return this.utils
.isCurrentOrParentOne(current
, parent
, tagName
);
8997 isCurrentOrParentOne: function (current
, parent
, tagName
) {
8998 tagName
= tagName
.toUpperCase();
9000 return parent
&& parent
.tagName
=== tagName
? parent
: current
&& current
.tagName
=== tagName
? current
: false;
9002 isEditorRelative: function () {
9003 var position
= this.core
.editor().css('position');
9004 var arr
= ['absolute', 'fixed', 'relative'];
9006 return ($.inArray(arr
, position
) !== -1);
9008 setEditorRelative: function () {
9009 this.core
.editor().addClass('redactor-relative');
9011 getScrollTarget: function () {
9012 var $scrollTarget
= $(this.opts
.scrollTarget
);
9014 return ($scrollTarget
.length
!== 0) ? $scrollTarget
: $(document
);
9016 freezeScroll: function () {
9017 this.freezeScrollTop
= this.utils
.getScrollTarget().scrollTop();
9018 this.utils
.getScrollTarget().scrollTop(this.freezeScrollTop
);
9020 unfreezeScroll: function () {
9021 if (typeof this.freezeScrollTop
=== 'undefined') {
9025 this.utils
.getScrollTarget().scrollTop(this.freezeScrollTop
);
9027 saveScroll: function () {
9028 this.tmpScrollTop
= this.utils
.getScrollTarget().scrollTop();
9030 restoreScroll: function () {
9031 if (typeof this.tmpScrollTop
=== 'undefined') {
9035 this.utils
.getScrollTarget().scrollTop(this.tmpScrollTop
);
9037 isStartOfElement: function (element
) {
9038 if (typeof element
=== 'undefined') {
9039 element
= this.selection
.block();
9045 return (this.offset
.get(element
) === 0) ? true : false;
9047 isEndOfElement: function (element
) {
9048 if (typeof element
=== 'undefined') {
9049 element
= this.selection
.block();
9055 var text
= $.trim($(element
).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
9058 var offset
= this.offset
.get(element
);
9060 return (offset
=== text
.length
) ? true : false;
9062 removeEmptyAttr: function (el
, attr
) {
9064 if (typeof $el
.attr(attr
) === 'undefined') {
9068 if ($el
.attr(attr
) === '') {
9069 $el
.removeAttr(attr
);
9075 replaceToTag: function (node
, tag
) {
9077 $(node
).replaceWith(function () {
9078 replacement
= $('<' + tag
+ ' />').append($(this).contents());
9080 for (var i
= 0; i
< this.attributes
.length
; i
++) {
9081 replacement
.attr(this.attributes
[i
].name
,
9082 this.attributes
[i
].value
9091 isSelectAll: function () {
9092 return this.selectAll
;
9094 enableSelectAll: function () {
9095 this.selectAll
= true;
9097 disableSelectAll: function () {
9098 this.selectAll
= false;
9100 disableBodyScroll: function () {},
9101 measureScrollbar: function () {
9102 var $body
= $('body');
9103 var scrollDiv
= document
.createElement('div');
9104 scrollDiv
.className
= 'redactor-scrollbar-measure';
9106 $body
.append(scrollDiv
);
9107 var scrollbarWidth
= scrollDiv
.offsetWidth
- scrollDiv
.clientWidth
;
9108 $body
[0].removeChild(scrollDiv
);
9109 return scrollbarWidth
;
9111 enableBodyScroll: function () {},
9112 appendFields: function (appendFields
, data
) {
9113 if (!appendFields
) {
9116 else if (typeof appendFields
=== 'object') {
9117 $.each(appendFields
, function (k
, v
) {
9118 if (v
!== null && v
.toString().indexOf('#') === 0) {
9129 var $fields
= $(appendFields
);
9130 if ($fields
.length
=== 0) {
9135 $fields
.each(function () {
9136 data
.append($(this).attr('name'), $(this).val());
9142 appendForms: function (appendForms
, data
) {
9147 var $forms
= $(appendForms
);
9148 if ($forms
.length
=== 0) {
9152 var formData
= $forms
.serializeArray();
9154 $.each(formData
, function (z
, f
) {
9155 data
.append(f
.name
, f
.value
);
9161 isRgb: function (str
) {
9162 return (str
.search(/^rgb/i) === 0);
9164 rgb2hex: function (rgb
) {
9166 /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
9168 return (rgb
&& rgb
.length
=== 4) ? '#' + ('0' + parseInt(rgb
[1],
9170 ).toString(16)).slice(-2) + ('0' + parseInt(rgb
[2],
9172 ).toString(16)).slice(-2) + ('0' + parseInt(rgb
[3],
9174 ).toString(16)).slice(-2) : '';
9178 isCollapsed: function () {
9179 return this.selection
.isCollapsed();
9181 isMobile: function () {
9182 return this.detect
.isMobile();
9184 isDesktop: function () {
9185 return this.detect
.isDesktop();
9187 isPad: function () {
9188 return this.detect
.isIpad();
9195 browser: function () {
9197 webkit: function () {
9198 return this.detect
.isWebkit();
9201 return this.detect
.isFirefox();
9204 return this.detect
.isIe();
9210 $(window
).on('load.tools.redactor', function () {
9211 $('[data-tools="redactor"]').redactor();
9215 Redactor
.prototype.init
.prototype = Redactor
.prototype;
9220 $.fn
.redactorAnimation = function (animation
, options
, callback
) {
9221 return this.each(function () {
9222 new redactorAnimation(this, animation
, options
, callback
);
9226 function redactorAnimation(element
, animation
, options
, callback
) {
9232 prefix
: 'redactor-',
9236 this.animation
= animation
;
9237 this.slide
= (this.animation
=== 'slideDown' || this.animation
=== 'slideUp');
9238 this.$element
= $(element
);
9239 this.prefixes
= ['', '-moz-', '-o-animation-', '-webkit-'];
9242 // options or callback
9243 if (typeof options
=== 'function') {
9248 this.opts
= $.extend(opts
, options
);
9253 this.$element
.height(this.$element
.height());
9257 this.init(callback
);
9261 redactorAnimation
.prototype = {
9263 init: function (callback
) {
9264 this.queue
.push(this.animation
);
9268 if (this.animation
=== 'show') {
9269 this.opts
.timing
= 'linear';
9270 this.$element
.removeClass('hide').show();
9272 if (typeof callback
=== 'function') {
9276 else if (this.animation
=== 'hide') {
9277 this.opts
.timing
= 'linear';
9278 this.$element
.hide();
9280 if (typeof callback
=== 'function') {
9285 this.animate(callback
);
9289 animate: function (callback
) {
9290 this.$element
.addClass('redactor-animated').css('display', '').removeClass('hide');
9291 this.$element
.addClass(this.opts
.prefix
+ this.queue
[0]);
9293 this.set(this.opts
.duration
+ 's', this.opts
.delay
+ 's', this.opts
.iterate
, this.opts
.timing
);
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])) {
9301 if (this.queue
.length
) {
9302 this.animate(callback
);
9306 }, this), _callback
);
9308 set: function (duration
, delay
, iterate
, timing
) {
9309 var len
= this.prefixes
.length
;
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
);
9319 clean: function () {
9320 this.$element
.removeClass('redactor-animated');
9321 this.$element
.removeClass(this.opts
.prefix
+ this.queue
[0]);
9323 this.set('', '', '', '');
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') {
9333 if (typeof callback
=== 'function') {
9346 if ($.inArray(this.animation
, effects
) !== -1) {
9347 this.$element
.css('display', 'none');
9352 this.$element
.css('height', '');