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) {
3638 if (e.target.tagName === 'IMG') {
3639 $(e.target).addClass('redactor-image-dragover');
3643 dragleave: function (e) {
3644 // remove image dragover
3645 this.core.editor().find('img').removeClass('redactor-image-dragover');
3647 drop: function (e) {
3648 e = e.originalEvent || e;
3650 // remove image dragover
3651 this.core.editor().find('img').removeClass('redactor-image-dragover');
3653 if (this.opts.type === 'inline' || this.opts.type === 'pre') {
3658 if (window.FormData === undefined || !e.dataTransfer) {
3662 if (e.dataTransfer.files.length === 0) {
3663 return this.events.onDrop(e);
3666 this.events.onDropUpload(e);
3669 this.core.callback('drop', e);
3672 click: function (e) {
3673 var event = this.core.getEvent();
3674 var type = (event === 'click' || event === 'arrow') ? false : 'click';
3676 this.core.addEvent(type);
3677 this.utils.disableSelectAll();
3678 this.core.callback('click', e);
3680 focus: function (e) {
3681 if (this.rtePaste) {
3685 if (this.events.isCallback('focus')) {
3686 this.core.callback('focus', e);
3689 this.events.focused = true;
3690 this.events.blured = false;
3693 if (this.selection.current() === false) {
3694 var sel = this.selection.get();
3695 var range = this.selection.range(sel);
3697 range.setStart(this.core.editor()[0], 0);
3698 range.setEnd(this.core.editor()[0], 0);
3699 this.selection.update(sel, range);
3703 blur: function (e) {
3704 if (this.start || this.rtePaste) {
3708 if ($(e.target).closest('#' + this.core.id() + ', .redactor-toolbar, .redactor-dropdown, #redactor-modal-box').length !== 0) {
3712 if (!this.events.blured && this.events.isCallback('blur')) {
3713 this.core.callback('blur', e);
3716 this.events.focused = false;
3717 this.events.blured = true;
3719 touchImageEditing: function () {
3720 var scrollTimer = -1;
3721 this.events.imageEditing = false;
3722 $(window).on('touchmove.redactor.' + this.uuid, $.proxy(function () {
3723 this.events.imageEditing = true;
3724 if (scrollTimer !== -1) {
3725 clearTimeout(scrollTimer);
3728 scrollTimer = setTimeout($.proxy(function () {
3729 this.events.imageEditing = false;
3736 this.core.editor().on('dragover.redactor dragenter.redactor',
3737 $.proxy(this.events.dragover, this)
3739 this.core.editor().on('dragleave.redactor',
3740 $.proxy(this.events.dragleave, this)
3742 this.core.editor().on('drop.redactor', $.proxy(this.events.drop, this));
3743 this.core.editor().on('click.redactor', $.proxy(this.events.click, this));
3744 this.core.editor().on('paste.redactor', $.proxy(this.paste.init, this));
3745 this.core.editor().on('keydown.redactor', $.proxy(this.keydown.init, this));
3746 this.core.editor().on('keyup.redactor', $.proxy(this.keyup.init, this));
3747 this.core.editor().on('focus.redactor', $.proxy(this.events.focus, this));
3749 $(document).on('mousedown.redactor-blur.' + this.uuid,
3750 $.proxy(this.events.blur, this)
3753 this.events.touchImageEditing();
3755 this.events.createObserver();
3756 this.events.setupObserver();
3759 createObserver: function () {
3761 this.events.observer = new MutationObserver(function (mutations) {
3762 mutations.forEach($.proxy(self.events.iterateObserver, self));
3766 iterateObserver: function (mutation) {
3771 if (((this.opts.type === 'textarea' || this.opts.type === 'div') && (!this.detect.isFirefox() && mutation.target === this.core.editor()[0])) || (mutation.attributeName === 'class' && mutation.target === this.core.editor()[0]) || (mutation.attributeName == 'data-vivaldi-spatnav-clickable')) {
3776 this.observe.load();
3777 this.events.changeHandler();
3780 setupObserver: function () {
3781 this.events.observer.observe(this.core.editor()[0], {
3785 characterData: true,
3786 characterDataOldValue: true
3789 changeHandler: function () {
3790 if (this.events.stopChanges) {
3797 onDropUpload: function (e) {
3799 e.stopPropagation();
3801 if ((!this.opts.dragImageUpload && !this.opts.dragFileUpload) || (this.opts.imageUpload === null && this.opts.fileUpload === null)) {
3805 if (e.target.tagName === 'IMG') {
3806 this.events.dropImage = e.target;
3809 var files = e.dataTransfer.files;
3810 var len = files.length;
3811 for (var i = 0; i < len; i++) {
3812 this.upload.directUpload(files[i], e);
3815 onDrop: function (e) {
3816 this.core.callback('drop', e);
3818 isCallback: function (name) {
3819 return (typeof this.opts.callbacks[name] !== 'undefined' && $.isFunction(this.opts.callbacks[name]));
3823 stopDetect: function () {
3824 this.events.stopDetectChanges();
3826 startDetect: function () {
3827 this.events.startDetectChanges();
3833 // =file -- UNSUPPORTED MODULE
3837 show: function () {},
3838 insert: function () {},
3839 release: function () {},
3840 text: function (json) {}
3845 focus: function () {
3847 start: function () {
3848 this.core.editor().focus();
3850 if (this.opts.type === 'inline') {
3854 var $first = this.focus.first();
3855 if ($first !== false) {
3856 this.caret.start($first);
3860 this.core.editor().focus();
3862 var last = (this.opts.inline) ? this.core.editor() : this.focus.last();
3863 if (last.length === 0) {
3867 // get inline last node
3868 var lastNode = this.focus.lastChild(last);
3869 if (!this.detect.isWebkit() && lastNode !== false) {
3870 this.caret.end(lastNode);
3873 var sel = this.selection.get();
3874 var range = this.selection.range(sel);
3876 if (range !== null) {
3877 range.selectNodeContents(last[0]);
3878 range.collapse(false);
3880 this.selection.update(sel, range);
3883 this.caret.end(last);
3888 first: function () {
3889 var $first = this.core.editor().children().first();
3890 if ($first.length === 0 && ($first[0].length === 0 || $first[0].tagName === 'BR' || $first[0].tagName === 'HR' || $first[0].nodeType === 3)) {
3894 if ($first[0].tagName === 'UL' || $first[0].tagName === 'OL') {
3895 return $first.find('li').first();
3902 return this.core.editor().children().last();
3904 lastChild: function (last) {
3905 var lastNode = last[0].lastChild;
3907 return (lastNode !== null && this.utils.isInlineTag(lastNode.tagName)) ? lastNode : false;
3910 return (this.core.editor()[0] === document.activeElement);
3916 image: function () {
3919 return !(!this.opts.imageUpload || !this.opts.imageUpload && !this.opts.s3);
3923 this.modal.load('image', this.lang.get('image'), 700);
3926 this.upload.init('#redactor-modal-image-droparea',
3927 this.opts.imageUpload,
3933 insert: function (json, direct, e) {
3937 if (typeof json.error !== 'undefined') {
3939 this.events.dropImage = false;
3940 this.core.callback('imageUploadError', json, e);
3945 if (this.events.dropImage !== false) {
3946 $img = $(this.events.dropImage);
3948 this.core.callback('imageDelete', $img[0].src, $img);
3950 $img.attr('src', json.url);
3952 this.events.dropImage = false;
3953 this.core.callback('imageUpload', $img, json);
3957 var $figure = $('<' + this.opts.imageTag + '>');
3960 $img.attr('src', json.url);
3963 var id = (typeof json.id === 'undefined') ? '' : json.id;
3964 var type = (typeof json.s3 === 'undefined') ? 'image' : 's3';
3965 $img.attr('data-' + type, id);
3967 $figure.append($img);
3969 var pre = this.utils.isTag(this.selection.current(), 'pre');
3972 this.marker.remove();
3974 var node = this.insert.nodeToPoint(e, this.marker.get());
3975 var $next = $(node).next();
3977 this.selection.restore();
3983 if (typeof $next !== 'undefined' && $next.length !== 0 && $next[0].tagName === 'IMG') {
3985 this.core.callback('imageDelete', $next[0].src, $next);
3988 $next.closest('figure, p', this.core.editor()[0]).replaceWith(
3990 this.caret.after($figure);
3994 $(pre).after($figure);
3997 this.insert.node($figure);
4000 this.caret.after($figure);
4012 $(pre).after($figure);
4015 this.insert.node($figure);
4018 this.caret.after($figure);
4021 this.events.dropImage = false;
4023 var nextNode = $img[0].nextSibling;
4024 var $nextFigure = $figure.next();
4025 var isNextEmpty = $(nextNode).text().replace(/\u200B/g, '');
4026 var isNextFigureEmpty = $nextFigure.text().replace(/\u200B/g, '');
4028 if (isNextEmpty === '') {
4029 $(nextNode).remove();
4032 if ($nextFigure.length === 1 && $nextFigure[0].tagName === 'FIGURE' && isNextFigureEmpty === '') {
4033 $nextFigure.remove();
4036 if (direct !== null) {
4037 this.core.callback('imageUpload', $img, json);
4040 this.core.callback('imageInserted', $img, json);
4043 setEditable: function ($image) {
4044 $image.on('dragstart', function (e) {
4048 if (this.opts.imageResizable) {
4049 var handler = $.proxy(function (e) {
4050 this.observe.image = $image;
4051 this.image.resizer = this.image.loadEditableControls($image);
4053 $(document).on('mousedown.redactor-image-resize-hide.' + this.uuid,
4054 $.proxy(this.image.hideResize, this)
4057 if (this.image.resizer) {
4058 this.image.resizer.on(
4059 'mousedown.redactor touchstart.redactor',
4060 $.proxy(function (e) {
4061 this.image.setResizable(e, $image);
4068 $image.off('mousedown.redactor').on('mousedown.redactor',
4069 $.proxy(this.image.hideResize, this)
4071 $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor',
4076 $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor',
4077 $.proxy(function (e) {
4078 setTimeout($.proxy(function () {
4079 this.image.showEdit($image);
4088 setResizable: function (e, $image) {
4091 this.image.resizeHandle = {
4095 ratio: $image.width() / $image.height(),
4099 e = e.originalEvent || e;
4101 if (e.targetTouches) {
4102 this.image.resizeHandle.x = e.targetTouches[0].pageX;
4103 this.image.resizeHandle.y = e.targetTouches[0].pageY;
4106 this.image.startResize();
4108 startResize: function () {
4109 $(document).on('mousemove.redactor-image-resize touchmove.redactor-image-resize',
4110 $.proxy(this.image.moveResize, this)
4112 $(document).on('mouseup.redactor-image-resize touchend.redactor-image-resize',
4113 $.proxy(this.image.stopResize, this)
4116 moveResize: function (e) {
4119 e = e.originalEvent || e;
4121 var height = this.image.resizeHandle.h;
4123 if (e.targetTouches) height += (e.targetTouches[0].pageY - this.image.resizeHandle.y); else height += (e.pageY - this.image.resizeHandle.y);
4125 var width = Math.round(height * this.image.resizeHandle.ratio);
4127 if (height < 50 || width < 100) return;
4128 if (this.core.editor().width() <= width) return;
4130 this.image.resizeHandle.el.attr({
4134 this.image.resizeHandle.el.width(width);
4135 this.image.resizeHandle.el.height(height);
4139 stopResize: function () {
4140 this.handle = false;
4141 $(document).off('.redactor-image-resize');
4143 this.image.hideResize();
4145 hideResize: function (e) {
4146 if (e && $(e.target).closest('#redactor-image-box',
4151 if (e && e.target.tagName == 'IMG') {
4152 var $image = $(e.target);
4155 var imageBox = this.$editor.find('#redactor-image-box');
4156 if (imageBox.length === 0) return;
4158 $('#redactor-image-editter').remove();
4159 $('#redactor-image-resizer').remove();
4161 imageBox.find('img').css({
4162 marginTop: imageBox[0].style.marginTop,
4163 marginBottom: imageBox[0].style.marginBottom,
4164 marginLeft: imageBox[0].style.marginLeft,
4165 marginRight: imageBox[0].style.marginRight
4168 imageBox.css('margin', '');
4169 imageBox.find('img').css('opacity', '');
4170 imageBox.replaceWith(function () {
4171 return $(this).contents();
4174 $(document).off('mousedown.redactor-image-resize-hide.' + this.uuid);
4176 if (typeof this.image.resizeHandle !== 'undefined') {
4177 this.image.resizeHandle.el.attr('rel',
4178 this.image.resizeHandle.el.attr('style')
4182 loadResizableControls: function ($image, imageBox) {
4183 if (this.opts.imageResizable && !this.detect.isMobile()) {
4184 var imageResizer = $(
4185 '<span id="redactor
-image
-resizer
" data-redactor="verified
"></span>');
4187 if (!this.detect.isDesktop()) {
4194 imageResizer.attr('contenteditable', false);
4195 imageBox.append(imageResizer);
4196 imageBox.append($image);
4198 return imageResizer;
4201 imageBox.append($image);
4205 loadEditableControls: function ($image) {
4206 if ($('#redactor-image-box').length !== 0) {
4210 var imageBox = $('<span id="redactor
-image
-box
" data-redactor="verified
">');
4211 imageBox.css('float', $image.css('float')).attr('contenteditable', false);
4213 if ($image[0].style.margin != 'auto') {
4215 marginTop: $image[0].style.marginTop,
4216 marginBottom: $image[0].style.marginBottom,
4217 marginLeft: $image[0].style.marginLeft,
4218 marginRight: $image[0].style.marginRight
4221 $image.css('margin', '');
4230 $image.css('opacity', '.5').after(imageBox);
4232 if (this.opts.imageEditable) {
4234 this.image.editter = $(
4235 '<span id="redactor
-image
-editter
" data-redactor="verified
">' + this.lang.get(
4236 'edit') + '</span>');
4237 this.image.editter.attr('contenteditable', false);
4238 this.image.editter.on('click', $.proxy(function () {
4239 this.image.showEdit($image);
4242 imageBox.append(this.image.editter);
4244 // position correction
4245 var editerWidth = this.image.editter.innerWidth();
4246 this.image.editter.css('margin-left', '-' + editerWidth / 2 + 'px');
4249 return this.image.loadResizableControls($image, imageBox);
4252 showEdit: function ($image) {
4253 if (this.events.imageEditing) {
4257 this.observe.image = $image;
4259 var $link = $image.closest('a', this.$editor[0]);
4260 var $figure = $image.closest('figure', this.$editor[0]);
4261 var $container = ($figure.length !== 0) ? $figure : $image;
4263 this.modal.load('image-edit', this.lang.get('edit'), 705);
4265 this.image.buttonDelete = this.modal.getDeleteButton().text(this.lang.get(
4267 this.image.buttonSave = this.modal.getActionButton().text(this.lang.get('save'));
4269 this.image.buttonDelete.on('click', $.proxy(this.image.remove, this));
4270 this.image.buttonSave.on('click', $.proxy(this.image.update, this));
4272 if (this.opts.imageCaption === false) {
4273 $('#redactor-image-caption').val('').hide().prev().hide();
4276 var $parent = $image.closest(this.opts.imageTag, this.$editor[0]);
4277 var $ficaption = $parent.find('figcaption');
4278 if ($ficaption !== 0) {
4280 $('#redactor-image-caption').val($ficaption.text()).show();
4284 if (!this.opts.imagePosition) {
4285 $('.redactor-image-position-option').hide();
4288 var isCentered = ($figure.length !== 0) ? ($container.css('text-align') === 'center') : ($container.css(
4289 'display') == 'block' && $container.css('float') == 'none');
4290 var floatValue = (isCentered) ? 'center' : $container.css('float');
4291 $('#redactor-image-align').val(floatValue);
4294 $('#redactor-image-preview').html($('<img src="' + $image.attr('src
') + '" style="max
-width
: 100%;">'));
4295 $('#redactor-image-title').val($image.attr('alt'));
4297 if ($link.length !== 0) {
4298 $('#redactor-image-link').val($link.attr('href'));
4299 if ($link.attr('target') === '_blank') {
4300 $('#redactor-image-link-blank').prop('checked', true);
4304 // hide link's tooltip
4305 $('.redactor-link-tooltip').remove();
4310 if (this.detect.isDesktop()) {
4311 $('#redactor-image-title').focus();
4315 update: function () {
4316 var $image = this.observe.image;
4317 var $link = $image.closest('a', this.core.editor()[0]);
4319 var title = $('#redactor-image-title').val().replace(/(<([^>]+)>)/ig, '');
4320 $image.attr('alt', title).attr('title', title);
4322 this.image.setFloating($image);
4325 var link = $.trim($('#redactor-image-link').val()).replace(/(<([^>]+)>)/ig, '');
4327 // test url (add protocol)
4328 var pattern = '((xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}';
4329 var re = new RegExp('^(http|ftp|https)://' + pattern, 'i');
4330 var re2 = new RegExp('^' + pattern, 'i');
4332 if (link.search(re) === -1 && link.search(re2) === 0 && this.opts.linkProtocol) {
4333 link = this.opts.linkProtocol + '://' + link;
4336 var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false;
4338 if ($link.length === 0) {
4339 var a = $('<a href="' + link + '" id="redactor
-img
-tmp
">' + this.utils.getOuterHtml(
4342 a.attr('target', '_blank');
4345 $image = $image.replaceWith(a);
4346 $link = this.core.editor().find('#redactor-img-tmp');
4347 $link.removeAttr('id');
4350 $link.attr('href', link);
4352 $link.attr('target', '_blank');
4355 $link.removeAttr('target');
4359 else if ($link.length !== 0) {
4360 $link.replaceWith(this.utils.getOuterHtml($image));
4363 this.image.addCaption($image, $link);
4370 setFloating: function ($image) {
4371 var $figure = $image.closest('figure', this.$editor[0]);
4372 var $container = ($figure.length !== 0) ? $figure : $image;
4373 var floating = $('#redactor-image-align').val();
4375 var imageFloat = '';
4376 var imageDisplay = '';
4377 var imageMargin = '';
4382 imageFloat = 'left';
4383 imageMargin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0';
4386 imageFloat = 'right';
4387 imageMargin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin;
4391 if ($figure.length !== 0) {
4392 textAlign = 'center';
4395 imageDisplay = 'block';
4396 imageMargin = 'auto';
4403 'float': imageFloat,
4404 'display': imageDisplay,
4405 'margin': imageMargin,
4406 'text-align': textAlign
4408 $container.attr('rel', $image.attr('style'));
4410 addCaption: function ($image, $link) {
4411 var caption = $('#redactor-image-caption').val();
4413 var $target = ($link.length !== 0) ? $link : $image;
4414 var $figcaption = $target.next();
4416 if ($figcaption.length === 0 || $figcaption[0].tagName !== 'FIGCAPTION') {
4417 $figcaption = false;
4420 if (caption !== '') {
4421 if ($figcaption === false) {
4422 $figcaption = $('<figcaption />').text(caption);
4423 $target.after($figcaption);
4426 $figcaption.text(caption);
4429 else if ($figcaption !== false) {
4430 $figcaption.remove();
4433 remove: function (e, $image, index) {
4434 $image = (typeof $image === 'undefined') ? $(this.observe.image) : $image;
4436 // delete from modal
4437 if (typeof e !== 'boolean') {
4441 this.events.stopDetectChanges();
4443 var $link = $image.closest('a', this.core.editor()[0]);
4444 var $figure = $image.closest(this.opts.imageTag, this.core.editor()[0]);
4445 var $parent = $image.parent();
4448 var imageDeleteStop = this.core.callback('imageDelete', e, $image[0]);
4449 if (imageDeleteStop === false) {
4450 if (e) e.preventDefault();
4454 if ($('#redactor-image-box').length !== 0) {
4455 $parent = $('#redactor-image-box').parent();
4459 if ($figure.length !== 0) {
4460 $prev = $figure.prev();
4461 $next = $figure.next();
4464 else if ($link.length !== 0) {
4465 $parent = $link.parent();
4472 $('#redactor-image-box').remove();
4475 if ($next && $next.length !== 0) {
4476 this.caret.start($next);
4478 else if ($prev && $prev.length !== 0) {
4479 this.caret.end($prev);
4483 if (typeof e !== 'boolean') {
4487 this.utils.restoreScroll();
4488 this.observe.image = false;
4489 this.events.startDetectChanges();
4497 indent: function () {
4499 increase: function () {
4500 if (!this.list.get()) {
4504 var $current = $(this.selection.current()).closest('li');
4505 var $list = $current.closest('ul, ol', this.core.editor()[0]);
4507 var $li = $current.closest('li');
4508 var $prev = $li.prev();
4509 if ($prev.length === 0 || $prev[0].tagName !== 'LI') {
4515 if (this.utils.isCollapsed()) {
4516 var listTag = $list[0].tagName;
4517 var $newList = $('<' + listTag + ' />');
4519 this.selection.save();
4521 var $ol = $prev.find('ol').first();
4522 if ($ol.length === 1) {
4523 $ol.append($current);
4526 var listTag = $list[0].tagName;
4527 var $newList = $('<' + listTag + ' />');
4528 $newList.append($current);
4529 $prev.append($newList);
4532 this.selection.restore();
4535 document.execCommand('indent');
4538 this.selection.save();
4539 this.indent.removeEmpty();
4540 this.indent.normalize();
4541 this.selection.restore();
4544 decrease: function () {
4545 if (!this.list.get()) {
4549 var $current = $(this.selection.current()).closest('li');
4550 var $list = $current.closest('ul, ol', this.core.editor()[0]);
4554 document.execCommand('outdent');
4556 var $item = $(this.selection.current()).closest('li', this.core.editor()[0]);
4558 if (this.utils.isCollapsed()) {
4559 this.indent.repositionItem($item);
4562 var tmpWrapper = null;
4563 if ($item.length === 0) {
4564 // `formatBlock` does not handle custom elements gracefully, always
4565 // treating them as inline elements, causing these to be chopped up
4566 // into separate elements. We can mitigate this problem by introducing
4567 // a temporary intermediate `<div>` which will serve as a block wrapper.
4568 var block = this.selection.block();
4569 if (block && block.nodeName.indexOf('-') !== -1) {
4570 this.selection.save();
4572 tmpWrapper = elCreate('div');
4573 while (block.childNodes.length) {
4574 tmpWrapper.appendChild(block.childNodes[0]);
4576 block.appendChild(tmpWrapper);
4578 this.selection.restore();
4581 document.execCommand('formatblock', false, 'p');
4582 $item = $(this.selection.current());
4583 var $next = $item.next();
4584 if ($next.length !== 0 && $next[0].tagName === 'BR') {
4590 this.selection.save();
4592 if (tmpWrapper !== null) {
4593 var parent = tmpWrapper.parentNode;
4594 while (tmpWrapper.childNodes.length) {
4595 parent.insertBefore(tmpWrapper.childNodes[0], tmpWrapper);
4597 parent.removeChild(tmpWrapper);
4600 this.indent.removeEmpty();
4601 this.indent.normalize();
4602 this.selection.restore();
4605 repositionItem: function ($item) {
4606 var $next = $item.next();
4607 if ($next.length !== 0 && ($next[0].tagName !== 'UL' || $next[0].tagName !== 'OL')) {
4608 $item.append($next);
4611 var $prev = $item.prev();
4612 if ($prev.length !== 0 && $prev[0].tagName !== 'LI') {
4613 this.selection.save();
4614 var $li = $item.parents('li', this.core.editor()[0]);
4616 this.selection.restore();
4619 normalize: function () {
4620 this.core.editor().find('li').each($.proxy(function (i, s) {
4625 if (this.opts.keepStyleAttr.length !== 0) {
4626 filter = ',' + this.opts.keepStyleAttr.join(',');
4629 $el.find(this.opts.inlineTags.join(',')).not('img' + filter).removeAttr(
4632 var $parent = $el.parent();
4633 if ($parent.length !== 0 && $parent[0].tagName === 'LI') {
4638 var $next = $el.next();
4639 if ($next.length !== 0 && ($next[0].tagName === 'UL' || $next[0].tagName === 'OL')) {
4646 removeEmpty: function ($list) {
4647 var $lists = this.core.editor().find('ul, ol');
4648 var $items = this.core.editor().find('li');
4650 $items.each($.proxy(function (i, s) {
4651 this.indent.removeItemEmpty(s);
4655 $lists.each($.proxy(function (i, s) {
4656 this.indent.removeItemEmpty(s);
4660 $items.each($.proxy(function (i, s) {
4661 this.indent.removeItemEmpty(s);
4665 removeItemEmpty: function (s) {
4666 var html = s.innerHTML.replace(/[\t\s\n]/g, '');
4667 html = html.replace(/<span><\/span>/g, '');
4677 inline: function () {
4679 format: function (tag, attr, value, type) {
4680 // Stop formatting pre/code
4681 if (this.utils.isCurrentOrParent(['PRE', 'CODE'])) return;
4684 var params = this.inline.getParams(attr, value, type);
4687 tag = this.inline.arrangeTag(tag);
4691 (this.utils.isCollapsed()) ? this.inline.formatCollapsed(tag,
4693 ) : this.inline.formatUncollapsed(tag, params);
4695 formatCollapsed: function (tag, params) {
4697 var inline = this.selection.inline();
4700 var currentTag = inline.tagName.toLowerCase();
4701 if (currentTag === tag) {
4703 if (this.utils.isEmpty(inline.innerHTML)) {
4704 this.caret.after(inline);
4707 // not empty = break
4709 var $first = this.inline.insertBreakpoint(inline,
4712 this.caret.after($first);
4715 else if ($(inline).closest(tag).length === 0) {
4716 newInline = this.inline.insertInline(tag);
4717 newInline = this.inline.setParams(newInline, params);
4720 var $first = this.inline.insertBreakpoint(inline, currentTag);
4721 this.caret.after($first);
4725 newInline = this.inline.insertInline(tag);
4726 newInline = this.inline.setParams(newInline, params);
4729 formatUncollapsed: function (tag, params) {
4732 this.selection.save();
4734 var range = window.getSelection().getRangeAt(0);
4735 var contents = range.cloneContents();
4736 if (contents.querySelector(tag) === null) {
4737 element = range.startContainer;
4738 if (element.nodeType === Node.TEXT_NODE) {
4739 element = element.parentElement;
4742 var parentWithTheSameTag = element.closest(tag);
4743 if (parentWithTheSameTag !== null && this.core.editor()[0].contains(parentWithTheSameTag)) {
4744 // We need to split the matching parent element
4745 // by moving everything to the left and the right
4746 // into separate nodes.
4747 var leftElement = document.createElement(tag);
4748 parentWithTheSameTag.insertAdjacentElement("beforebegin
", leftElement);
4750 var leftRange = document.createRange();
4751 leftRange.selectNodeContents(parentWithTheSameTag);
4752 leftRange.setEnd(range.startContainer, range.startOffset);
4753 leftElement.appendChild(leftRange.extractContents());
4755 var rightElement = document.createElement(tag);
4756 parentWithTheSameTag.insertAdjacentElement("afterend
", rightElement);
4758 var rightRange = document.createRange();
4759 rightRange.selectNodeContents(parentWithTheSameTag);
4760 rightRange.setStart(range.endContainer, range.endOffset);
4761 rightElement.appendChild(rightRange.extractContents());
4763 // Finally remove the offending parent element.
4764 var parentElement = parentWithTheSameTag.parentElement;
4765 while (parentWithTheSameTag.childNodes.length) {
4766 parentElement.insertBefore(parentWithTheSameTag.childNodes[0], parentWithTheSameTag);
4768 parentWithTheSameTag.remove();
4774 var nodes = this.inline.getClearedNodes();
4775 this.inline.setNodesStriked(nodes, tag, params);
4777 this.selection.restore();
4779 document.execCommand('strikethrough');
4781 this.selection.saveInstant();
4783 // WoltLab: Chrome misbehaves in some cases, causing the `<strike>` element for
4784 // contained elements to be stripped. Instead, those children are assigned the
4785 // CSS style `text-decoration-line: line-through`.
4786 var chromeElements = this.core.editor()[0].querySelectorAll('[style*="line
-through
"]'), strike;
4787 for (var i = 0, length = chromeElements.length; i < length; i++) {
4788 element = chromeElements[0];
4790 strike = document.createElement('strike');
4791 element.parentNode.insertBefore(strike, element);
4792 strike.appendChild(element);
4794 // Remove the bogus style attribute.
4795 element.style.removeProperty('text-decoration');
4799 this.core.editor().find('strike').each(function () {
4800 var $el = self.utils.replaceToTag(this, tag);
4801 self.inline.setParams($el[0], params);
4803 var $inside = $el.find(tag);
4804 var $parent = $el.parent();
4805 var $parentAround = $parent.parent();
4807 // revert formatting (safari bug)
4808 if ($parentAround.length !== 0 && $parentAround[0].tagName.toLowerCase() === tag && $parentAround.html() == $parent[0].outerHTML) {
4809 $el.replaceWith(function () { return $(this).contents(); });
4810 $parentAround.replaceWith(function () { return $(this).contents(); });
4816 if ($inside.length !== 0) {
4817 self.inline.cleanInsideOrParent($inside, params);
4821 if ($parent.html() == $el[0].outerHTML) {
4822 self.inline.cleanInsideOrParent($parent, params);
4825 // bugfix: remove empty inline tags after selection
4826 if (self.detect.isFirefox()) {
4827 self.core.editor().find(tag + ':empty').remove();
4831 this.selection.restoreInstant();
4833 cleanInsideOrParent: function ($el, params) {
4835 for (var key in params.data) {
4836 this.inline.removeSpecificAttr($el, key, params.data[key]);
4840 getClearedNodes: function () {
4841 var nodes = this.selection.nodes();
4843 var len = nodes.length;
4847 for (var i = 0; i < len; i++) {
4848 if ($(nodes[i]).hasClass('redactor-selection-marker')) {
4854 // find selected inline & text nodes
4855 for (var i = 0; i < len; i++) {
4856 if (i >= started && !this.utils.isBlockTag(nodes[i].tagName)) {
4857 newNodes.push(nodes[i]);
4863 isConvertableAttr: function (node, name, value) {
4864 var nodeAttrValue = $(node).attr(name);
4865 if (nodeAttrValue) {
4866 if (name === 'style') {
4867 value = $.trim(value).replace(/;$/, '');
4869 var rules = value.split(';');
4871 for (var i = 0; i < rules.length; i++) {
4872 var arr = rules[i].split(':');
4873 var ruleName = $.trim(arr[0]);
4874 var ruleValue = $.trim(arr[1]);
4876 if (ruleName.search(/color/) !== -1) {
4877 var val = $(node).css(ruleName);
4878 if (val && (val === ruleValue || this.utils.rgb2hex(
4879 val) === ruleValue)) {
4883 else if ($(node).css(ruleName) === ruleValue) {
4888 if (count === rules.length) {
4892 else if (nodeAttrValue === value) {
4900 isConvertable: function (node, nodeTag, tag, params) {
4901 if (nodeTag === tag) {
4904 for (var key in params.data) {
4905 count += this.inline.isConvertableAttr(node,
4911 if (count === Object.keys(params.data).length) {
4922 setNodesStriked: function (nodes, tag, params) {
4923 for (var i = 0; i < nodes.length; i++) {
4924 var nodeTag = (nodes[i].tagName) ? nodes[i].tagName.toLowerCase() : undefined;
4926 var parent = nodes[i].parentNode;
4927 var parentTag = (parent && parent.tagName) ? parent.tagName.toLowerCase() : undefined;
4929 var convertable = this.inline.isConvertable(parent,
4935 var $el = $(parent).replaceWith(function () {
4936 return $('<strike>').append($(this).contents());
4939 $el.attr('data-redactor-inline-converted');
4942 var convertable = this.inline.isConvertable(nodes[i],
4948 var $el = $(nodes[i]).replaceWith(function () {
4949 return $('<strike>').append($(this).contents());
4954 insertBreakpoint: function (inline, currentTag) {
4955 var breakpoint = document.createElement('span');
4956 breakpoint.id = 'redactor-inline-breakpoint';
4957 breakpoint = this.insert.node(breakpoint);
4959 var end = this.utils.isEndOfElement(inline);
4960 var code = this.utils.getOuterHtml(inline);
4961 var endTag = (end) ? '' : '<' + currentTag + '>';
4963 code = code.replace(/<span id="redactor
-inline
-breakpoint
"><\/span>/i,
4964 '</' + currentTag + '>' + endTag
4967 var $code = $(code);
4968 $(inline).replaceWith($code);
4970 if (endTag !== '') {
4971 this.utils.cloneAttributes(inline, $code.last());
4974 return $code.first();
4976 insertInline: function (tag) {
4977 var node = document.createElement(tag);
4979 this.insert.node(node);
4980 this.caret.start(node);
4984 arrangeTag: function (tag) {
4997 'strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub'
5000 tag = tag.toLowerCase();
5002 for (var i = 0; i < tags.length; i++) {
5003 if (tag === tags[i]) {
5010 getStyleParams: function (params) {
5012 var rules = params.trim().replace(/;$/, '').split(';');
5013 for (var i = 0; i < rules.length; i++) {
5014 var rule = rules[i].split(':');
5016 result[rule[0].trim()] = rule[1].trim();
5022 getParams: function (attr, value, type) {
5024 var func = 'toggle';
5025 if (typeof attr === 'object') {
5027 func = (value !== undefined) ? value : func;
5029 else if (attr !== undefined && value !== undefined) {
5032 func = (type !== undefined) ? type : func;
5040 setParams: function (node, params) {
5042 for (var key in params.data) {
5043 var $node = $(node);
5044 if (key === 'style') {
5045 node = this.inline[params.func + 'Style'](params.data[key],
5048 $node.attr('data-redactor-style-cache',
5052 else if (key === 'class') {
5053 node = this.inline[params.func + 'Class'](params.data[key],
5059 node = (params.func === 'remove') ? this.inline[params.func + 'Attr'](key,
5061 ) : this.inline[params.func + 'Attr'](key,
5067 if (key === 'style' && node.tagName === 'SPAN') {
5068 $node.attr('data-redactor-span', true);
5077 eachInline: function (node, callback) {
5079 var nodes = (node === undefined) ? this.selection.inlines() : [node];
5081 for (var i = 0; i < nodes.length; i++) {
5082 lastNode = callback(nodes[i])[0];
5090 replaceClass: function (value, node) {
5091 return this.inline.eachInline(node, function (el) {
5092 return $(el).removeAttr('class').addClass(value);
5095 toggleClass: function (value, node) {
5096 return this.inline.eachInline(node, function (el) {
5097 return $(el).toggleClass(value);
5100 addClass: function (value, node) {
5101 return this.inline.eachInline(node, function (el) {
5102 return $(el).addClass(value);
5105 removeClass: function (value, node) {
5106 return this.inline.eachInline(node, function (el) {
5107 return $(el).removeClass(value);
5110 removeAllClass: function (node) {
5111 return this.inline.eachInline(node, function (el) {
5112 return $(el).removeAttr('class');
5117 replaceAttr: function (name, value, node) {
5118 return this.inline.eachInline(node, function (el) {
5119 return $(el).removeAttr(name).attr(name.value);
5122 toggleAttr: function (name, value, node) {
5123 return this.inline.eachInline(node, function (el) {
5124 var attr = $(el).attr(name);
5126 return (attr) ? $(el).removeAttr(name) : $(el).attr(name.value);
5129 addAttr: function (name, value, node) {
5130 return this.inline.eachInline(node, function (el) {
5131 return $(el).attr(name, value);
5134 removeAttr: function (name, node) {
5135 return this.inline.eachInline(node, function (el) {
5138 $el.removeAttr(name);
5139 if (name === 'style') {
5140 $el.removeAttr('data-redactor-style-cache');
5146 removeAllAttr: function (node) {
5147 return this.inline.eachInline(node, function (el) {
5149 var len = el.attributes.length;
5150 for (var z = 0; z < len; z++) {
5151 $el.removeAttr(el.attributes[z].name);
5157 removeSpecificAttr: function (node, key, value) {
5159 if (key === 'style') {
5160 var arr = value.split(':');
5161 var name = arr[0].trim();
5164 if (this.utils.removeEmptyAttr(node, 'style')) {
5165 $el.removeAttr('data-redactor-style-cache');
5169 $el.removeAttr(key)[0];
5174 hasParentStyle: function ($el) {
5175 var $parent = $el.parent();
5177 return ($parent.length === 1 && $parent[0].tagName === $el[0].tagName && $parent.html() === $el[0].outerHTML) ? $parent : false;
5179 addParentStyle: function ($el) {
5180 var $parent = this.inline.hasParentStyle($el);
5182 var style = this.inline.getStyleParams($el.attr('style'));
5184 $parent.attr('data-redactor-style-cache', $parent.attr('style'));
5186 $el.replaceWith(function () {
5187 return $(this).contents();
5191 $el.attr('data-redactor-style-cache', $el.attr('style'));
5196 replaceStyle: function (params, node) {
5197 params = this.inline.getStyleParams(params);
5200 return this.inline.eachInline(node, function (el) {
5202 $el.removeAttr('style').css(params);
5204 var style = $el.attr('style');
5205 if (style) $el.attr('style', style.replace(/"/g
, '\''));
5207 $el
= self
.inline
.addParentStyle($el
);
5212 toggleStyle: function (params
, node
) {
5213 params
= this.inline
.getStyleParams(params
);
5216 return this.inline
.eachInline(node
, function (el
) {
5219 for (var key
in params
) {
5220 var newVal
= params
[key
];
5221 var oldVal
= $el
.css(key
);
5223 oldVal
= (self
.utils
.isRgb(oldVal
)) ? self
.utils
.rgb2hex(oldVal
) : oldVal
.replace(/"/g,
5226 newVal = (self.utils.isRgb(newVal)) ? self.utils.rgb2hex(newVal) : newVal.replace(/"/g
,
5230 if (oldVal
=== newVal
) {
5234 $el
.css(key
, newVal
);
5238 var style
= $el
.attr('style');
5239 if (style
) $el
.attr('style', style
.replace(/"/g, '\''));
5241 if (!self.utils.removeEmptyAttr(el, 'style')) {
5242 $el = self.inline.addParentStyle($el);
5245 $el.removeAttr('data-redactor-style-cache');
5251 addStyle: function (params, node) {
5252 params = this.inline.getStyleParams(params);
5255 return this.inline.eachInline(node, function (el) {
5260 var style = $el.attr('style');
5261 if (style) $el.attr('style', style.replace(/"/g
, '\''));
5263 $el
= self
.inline
.addParentStyle($el
);
5268 removeStyle: function (params
, node
) {
5269 params
= this.inline
.getStyleParams(params
);
5272 return this.inline
.eachInline(node
, function (el
) {
5275 for (var key
in params
) {
5279 if (self
.utils
.removeEmptyAttr(el
, 'style')) {
5280 $el
.removeAttr('data-redactor-style-cache');
5283 $el
.attr('data-redactor-style-cache', $el
.attr('style'));
5289 removeAllStyle: function (node
) {
5290 return this.inline
.eachInline(node
, function (el
) {
5291 return $(el
).removeAttr('style').removeAttr('data-redactor-style-cache');
5294 removeStyleRule: function (name
) {
5295 var parent
= this.selection
.parent();
5296 var nodes
= this.selection
.inlines();
5300 if (parent
&& parent
.tagName
=== 'SPAN') {
5301 this.inline
.removeStyleRuleAttr($(parent
), name
);
5304 for (var i
= 0; i
< nodes
.length
; i
++) {
5307 if ($.inArray(el
.tagName
.toLowerCase(),
5308 this.opts
.inlineTags
5309 ) != -1 && !$el
.hasClass('redactor-selection-marker')) {
5310 this.inline
.removeStyleRuleAttr($el
, name
);
5315 removeStyleRuleAttr: function ($el
, name
) {
5317 if (this.utils
.removeEmptyAttr($el
, 'style')) {
5318 $el
.removeAttr('data-redactor-style-cache');
5321 $el
.attr('data-redactor-style-cache', $el
.attr('style'));
5326 update: function (tag
, attr
, value
, type
) {
5327 tag
= this.inline
.arrangeTag(tag
);
5329 var params
= this.inline
.getParams(attr
, value
, type
);
5330 var nodes
= this.selection
.inlines();
5334 for (var i
= 0; i
< nodes
.length
; i
++) {
5336 if (tag
=== '*' || el
.tagName
.toLowerCase() === tag
) {
5337 result
.push(this.inline
.setParams(el
, params
));
5346 removeFormat: function () {
5347 this.selection
.save();
5349 var nodes
= this.inline
.getClearedNodes();
5350 for (var i
= 0; i
< nodes
.length
; i
++) {
5351 if (nodes
[i
].nodeType
=== 1) {
5352 $(nodes
[i
]).replaceWith(function () {
5353 return $(this).contents();
5358 this.selection
.restore();
5365 insert: function () {
5367 set: function (html
) {
5368 this.code
.set(html
);
5371 html: function (html
, data
) {
5372 this.core
.editor().focus();
5374 var block
= this.selection
.block();
5375 var inline
= this.selection
.inline();
5378 if (typeof data
=== 'undefined') {
5379 data
= this.clean
.getCurrentType(html
, true);
5380 html
= this.clean
.onPaste(html
, data
, true);
5383 html
= $.parseHTML(html
);
5386 var endNode
= $(html
).last();
5388 // delete selected content
5389 var sel
= this.selection
.get();
5390 var range
= this.selection
.range(sel
);
5391 range
.deleteContents();
5393 this.selection
.update(sel
, range
);
5395 // insert list in list
5397 var $list
= $(html
);
5398 if ($list
.length
!== 0 && ($list
[0].tagName
=== 'UL' || $list
[0].tagName
=== 'OL')) {
5400 this.insert
.appendLists(block
, $list
);
5405 if (data
.blocks
&& block
) {
5406 if (this.utils
.isSelectAll()) {
5407 this.core
.editor().html(html
);
5411 var breaked
= this.utils
.breakBlockTag();
5412 if (breaked
=== false) {
5413 this.insert
.placeHtml(html
);
5416 var $last
= $(html
).children().last();
5417 $last
.append(this.marker
.get());
5419 if (breaked
.type
=== 'start') {
5420 breaked
.$block
.before(html
);
5423 breaked
.$block
.after(html
);
5426 this.selection
.restore();
5427 this.core
.editor().find('p').each(function () {
5428 if ($.trim(this.innerHTML
) === '') {
5437 // remove same tag inside
5438 var $div
= $('<div/>').html(html
);
5439 $div
.find(inline
.tagName
.toLowerCase()).each(function () {
5440 $(this).contents().unwrap();
5444 html
= $.parseHTML(html
);
5446 endNode
= $(html
).last();
5450 if (this.utils
.isSelectAll()) {
5451 var $node
= $(this.opts
.emptyHtml
);
5452 this.core
.editor().html('').append($node
);
5454 this.caret
.end($node
);
5457 this.insert
.placeHtml(html
);
5461 this.utils
.disableSelectAll();
5463 if (data
.pre
) this.clean
.cleanPre();
5465 this.caret
.end(endNode
);
5467 text: function (text
) {
5468 text
= text
.toString();
5469 text
= $.trim(text
);
5471 var tmp
= document
.createElement('div');
5472 tmp
.innerHTML
= text
;
5473 text
= tmp
.textContent
|| tmp
.innerText
;
5475 if (typeof text
=== 'undefined') {
5479 this.core
.editor().focus();
5482 var blocks
= this.selection
.blocks();
5485 text
= text
.replace(/\n/g, ' ');
5488 if (this.utils
.isSelectAll()) {
5489 var $node
= $(this.opts
.emptyHtml
);
5490 this.core
.editor().html('').append($node
);
5492 this.caret
.end($node
);
5496 var sel
= this.selection
.get();
5497 var node
= document
.createTextNode(text
);
5499 if (sel
.getRangeAt
&& sel
.rangeCount
) {
5500 var range
= sel
.getRangeAt(0);
5501 range
.deleteContents();
5502 range
.insertNode(node
);
5503 range
.setStartAfter(node
);
5504 range
.collapse(true);
5506 this.selection
.update(sel
, range
);
5509 // wrap node if selected two or more block tags
5510 if (blocks
.length
> 1) {
5511 $(node
).wrap('<p>');
5512 this.caret
.after(node
);
5516 this.utils
.disableSelectAll();
5517 this.clean
.normalizeCurrentHeading();
5520 raw: function (html
) {
5521 this.core
.editor().focus();
5523 var sel
= this.selection
.get();
5525 var range
= this.selection
.range(sel
);
5526 range
.deleteContents();
5528 var el
= document
.createElement('div');
5529 el
.innerHTML
= html
;
5531 var frag
= document
.createDocumentFragment(), node
, lastNode
;
5532 while ((node
= el
.firstChild
)) {
5533 lastNode
= frag
.appendChild(node
);
5536 range
.insertNode(frag
);
5539 range
= range
.cloneRange();
5540 range
.setStartAfter(lastNode
);
5541 range
.collapse(true);
5542 sel
.removeAllRanges();
5543 sel
.addRange(range
);
5546 node: function (node
, deleteContent
) {
5547 if (typeof this.start
!== 'undefined') {
5548 this.core
.editor().focus();
5551 node
= node
[0] || node
;
5553 var block
= this.selection
.block();
5554 var gap
= this.utils
.isBlockTag(node
.tagName
);
5557 if (this.utils
.isSelectAll()) {
5559 this.core
.editor().html(node
);
5562 this.core
.editor().html($('<p>').html(node
));
5567 else if (gap
&& block
) {
5568 var breaked
= this.utils
.breakBlockTag();
5569 if (breaked
=== false) {
5570 this.insert
.placeNode(node
, deleteContent
);
5573 if (breaked
.type
=== 'start') {
5574 breaked
.$block
.before(node
);
5577 breaked
.$block
.after(node
);
5580 this.core
.editor().find('p:empty').remove();
5584 result
= this.insert
.placeNode(node
, deleteContent
);
5587 this.utils
.disableSelectAll();
5590 this.caret
.end(node
);
5596 appendLists: function (block
, $list
) {
5597 var $block
= $(block
);
5599 var isEmpty
= this.utils
.isEmpty(block
.innerHTML
);
5601 if (isEmpty
|| this.utils
.isEndOfElement(block
)) {
5603 $list
.find('li').each(function () {
5612 else if (this.utils
.isStartOfElement(block
)) {
5613 $list
.find('li').each(function () {
5614 $block
.before(this);
5619 var endOfNode
= this.selection
.extractEndOfNode(block
);
5621 $block
.after($('<li>').append(endOfNode
));
5622 $block
.append($list
);
5626 this.marker
.remove();
5629 this.caret
.end(last
);
5632 placeHtml: function (html
) {
5633 var marker
= document
.createElement('span');
5634 marker
.id
= 'redactor-insert-marker';
5635 marker
= this.insert
.node(marker
);
5637 $(marker
).before(html
);
5638 this.selection
.restore();
5639 this.caret
.after(marker
);
5642 placeNode: function (node
, deleteContent
) {
5643 var sel
= this.selection
.get();
5644 var range
= this.selection
.range(sel
);
5645 if (range
== null) {
5649 if (deleteContent
!== false) {
5650 range
.deleteContents();
5653 range
.insertNode(node
);
5654 range
.collapse(false);
5656 this.selection
.update(sel
, range
);
5658 nodeToPoint: function (e
, node
) {
5659 node
= node
[0] || node
;
5661 if (this.utils
.isEmpty()) {
5662 node
= (this.utils
.isBlock(node
)) ? node
: $('<p />').append(node
);
5664 this.core
.editor().html(node
);
5670 var x
= e
.clientX
, y
= e
.clientY
;
5671 if (document
.caretPositionFromPoint
) {
5672 var pos
= document
.caretPositionFromPoint(x
, y
);
5673 var sel
= document
.getSelection();
5674 range
= sel
.getRangeAt(0);
5675 range
.setStart(pos
.offsetNode
, pos
.offset
);
5676 range
.collapse(true);
5677 range
.insertNode(node
);
5679 else if (document
.caretRangeFromPoint
) {
5680 range
= document
.caretRangeFromPoint(x
, y
);
5681 range
.insertNode(node
);
5683 else if (typeof document
.body
.createTextRange
!== 'undefined') {
5684 range
= document
.body
.createTextRange();
5685 range
.moveToPoint(x
, y
);
5686 var endRange
= range
.duplicate();
5687 endRange
.moveToPoint(x
, y
);
5688 range
.setEndPoint('EndToEnd', endRange
);
5697 nodeToCaretPositionFromPoint: function (e
, node
) {
5698 this.insert
.nodeToPoint(e
, node
);
5700 marker: function () {
5701 this.marker
.insert();
5707 keydown: function () {
5709 init: function (e
) {
5710 if (this.rtePaste
) {
5715 var arrow
= (key
>= 37 && key
<= 40);
5717 this.keydown
.ctrl
= e
.ctrlKey
|| e
.metaKey
;
5718 this.keydown
.parent
= this.selection
.parent();
5719 this.keydown
.current
= this.selection
.current();
5720 this.keydown
.block
= this.selection
.block();
5723 this.keydown
.pre
= this.utils
.isTag(this.keydown
.current
, 'pre');
5724 this.keydown
.blockquote
= this.utils
.isTag(this.keydown
.current
, 'blockquote');
5725 this.keydown
.figcaption
= this.utils
.isTag(this.keydown
.current
, 'figcaption');
5726 this.keydown
.figure
= this.utils
.isTag(this.keydown
.current
, 'figure');
5729 var keydownStop
= this.core
.callback('keydown', e
);
5730 if (keydownStop
=== false) {
5736 this.shortcuts
.init(e
, key
);
5739 this.keydown
.checkEvents(arrow
, key
);
5740 this.keydown
.setupBuffer(e
, key
);
5742 if (this.utils
.isSelectAll() && (key
=== this.keyCode
.ENTER
|| key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
)) {
5745 this.code
.set(this.opts
.emptyHtml
);
5746 this.events
.changeHandler();
5750 this.keydown
.addArrowsEvent(arrow
);
5751 this.keydown
.setupSelectAll(e
, key
);
5753 // turn off enter key
5754 if (!this.opts
.enterKey
&& key
=== this.keyCode
.ENTER
) {
5758 var sel
= this.selection
.get();
5759 var range
= this.selection
.range(sel
);
5761 if (!range
.collapsed
) {
5762 range
.deleteContents();
5769 if (this.opts
.enterKey
&& key
=== this.keyCode
.DOWN
) {
5770 this.keydown
.onArrowDown();
5774 if (this.opts
.enterKey
&& key
=== this.keyCode
.UP
) {
5775 this.keydown
.onArrowUp();
5778 // replace to p before / after the table or into body
5779 if ((this.opts
.type
=== 'textarea' || this.opts
.type
=== 'div') && this.keydown
.current
&& this.keydown
.current
.nodeType
=== 3 && $(
5780 this.keydown
.parent
).hasClass('redactor-in')) {
5781 this.keydown
.wrapToParagraph();
5784 // on Shift+Space or Ctrl+Space
5785 if (!this.keyup
.lastShiftKey
&& key
=== this.keyCode
.SPACE
&& (e
.ctrlKey
|| e
.shiftKey
)) {
5788 return this.keydown
.onShiftSpace();
5791 // on Shift+Enter or Ctrl+Enter
5792 if (key
=== this.keyCode
.ENTER
&& (e
.ctrlKey
|| e
.shiftKey
)) {
5793 // iOS Safari will report the shift key to be pressed, if the caret is at the
5794 // front of the line and the next character should be an uppercase character.
5795 if (Environment
=== null || Environment
.platform() !== 'ios') {
5798 return this.keydown
.onShiftEnter(e
);
5803 if (key
=== this.keyCode
.ENTER
&& !e
.shiftKey
&& !e
.ctrlKey
&& !e
.metaKey
) {
5804 return this.keydown
.onEnter(e
);
5808 if (key
=== this.keyCode
.TAB
|| e
.metaKey
&& key
=== 221 || e
.metaKey
&& key
=== 219) {
5809 return this.keydown
.onTab(e
, key
);
5813 if (this.detect
.isFirefox() && key
=== this.keyCode
.BACKSPACE
&& this.keydown
.block
&& this.keydown
.block
.tagName
=== 'P' && this.utils
.isStartOfElement(
5814 this.keydown
.block
)) {
5815 var $prev
= $(this.keydown
.block
).prev();
5816 if ($prev
.length
!== 0) {
5819 $prev
.append(this.marker
.get());
5820 $prev
.append($(this.keydown
.block
).html());
5821 $(this.keydown
.block
).remove();
5823 this.selection
.restore();
5829 // backspace & delete
5830 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
5831 if (this.observe
.image
&& typeof this.observe
.image
!== 'undefined' && $(
5832 '#redactor-image-box').length
!== 0) {
5835 var $prev
= this.observe
.image
.closest('figure, p').prev();
5836 this.image
.remove(false);
5837 this.observe
.image
= false;
5839 if ($prev
&& $prev
.length
!== 0) {
5840 this.caret
.end($prev
);
5843 this.core
.editor().focus();
5849 this.keydown
.onBackspaceAndDeleteBefore();
5852 if (key
=== this.keyCode
.DELETE
) {
5853 var $next
= $(this.keydown
.block
).next();
5856 if (this.utils
.isEndOfElement(this.keydown
.block
) && $next
.length
!== 0 && $next
[0].tagName
=== 'FIGURE') {
5861 // append list (safari bug)
5862 var tagLi
= (this.keydown
.block
&& this.keydown
.block
.tagName
=== 'LI') ? this.keydown
.block
: false;
5864 var $list
= $(this.keydown
.block
).parents('ul, ol').last();
5865 var $nextList
= $list
.next();
5867 if (this.utils
.isRedactorParent($list
) && this.utils
.isEndOfElement(
5868 $list
) && $nextList
.length
!== 0 && ($nextList
[0].tagName
=== 'UL' || $nextList
[0].tagName
=== 'OL')) {
5871 $list
.append($nextList
.contents());
5879 if (this.utils
.isEndOfElement(this.keydown
.block
) && $next
.length
!== 0 && $next
[0].tagName
=== 'PRE') {
5880 $(this.keydown
.block
).append($next
.text());
5888 if (key
=== this.keyCode
.DELETE
&& $('#redactor-image-box').length
!== 0) {
5889 this.image
.remove();
5893 if (key
=== this.keyCode
.BACKSPACE
) {
5894 if (this.detect
.isFirefox()) {
5895 this.line
.removeOnBackspace(e
);
5898 // combine list after and before if paragraph is empty
5899 if (this.list
.combineAfterAndBefore(this.keydown
.block
)) {
5904 // backspace as outdent
5905 var block
= this.selection
.block();
5906 if (block
&& block
.tagName
=== 'LI' && this.utils
.isCollapsed() && this.utils
.isStartOfElement()) {
5907 this.indent
.decrease();
5912 this.keydown
.removeInvisibleSpace();
5913 this.keydown
.removeEmptyListInTable(e
);
5917 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
5918 this.keydown
.onBackspaceAndDeleteAfter(e
);
5922 onShiftSpace: function () {
5924 this.insert
.raw(' ');
5928 onShiftEnter: function (e
) {
5931 return (this.keydown
.pre
) ? this.keydown
.insertNewLine(e
) : this.insert
.raw(
5934 onBackspaceAndDeleteBefore: function () {
5935 this.utils
.saveScroll();
5937 onBackspaceAndDeleteAfter: function (e
) {
5939 setTimeout($.proxy(function () {
5940 this.code
.syncFire
= false;
5941 this.keydown
.removeEmptyLists();
5944 if (this.opts
.keepStyleAttr
.length
!== 0) {
5945 filter
= ',' + this.opts
.keepStyleAttr
.join(',');
5948 var $styleTags
= this.core
.editor().find('*[style]');
5950 'img, figure, iframe, #redactor-image-box, #redactor-image-editter, [data-redactor-style-cache], [data-redactor-span]' + filter
).removeAttr(
5953 this.keydown
.formatEmpty(e
);
5954 this.code
.syncFire
= true;
5958 onEnter: function (e
) {
5959 var stop
= this.core
.callback('enter', e
);
5960 if (stop
=== false) {
5966 if (this.keydown
.blockquote
&& this.keydown
.exitFromBlockquote(e
) === true) {
5971 if (this.keydown
.pre
) {
5972 return this.keydown
.insertNewLine(e
);
5974 // blockquote & figcaption
5975 else if (this.keydown
.blockquote
|| this.keydown
.figcaption
) {
5976 return this.keydown
.insertBreakLine(e
);
5979 else if (this.keydown
.figure
) {
5980 setTimeout($.proxy(function () {
5981 this.keydown
.replaceToParagraph('FIGURE');
5986 else if (this.keydown
.block
) {
5987 setTimeout($.proxy(function () {
5988 this.keydown
.replaceToParagraph('DIV');
5993 if (this.keydown
.block
.tagName
=== 'LI') {
5994 var current
= this.selection
.current();
5995 var $parent
= $(current
).closest('li', this.$editor
[0]);
5996 var $list
= $parent
.parents('ul,ol', this.$editor
[0]).last();
5998 if ($parent
.length
!== 0 && this.utils
.isEmpty($parent
.html()) && $list
.next().length
=== 0 && this.utils
.isEmpty(
5999 $list
.find('li').last().html())) {
6000 $list
.find('li').last().remove();
6002 var node
= $(this.opts
.emptyHtml
);
6004 this.caret
.start(node
);
6012 else if (!this.keydown
.block
) {
6013 return this.keydown
.insertParagraph(e
);
6016 // firefox enter into inline element
6017 if (this.detect
.isFirefox() && this.utils
.isInline(this.keydown
.parent
)) {
6018 this.keydown
.insertBreakLine(e
);
6022 // remove inline tags in new-empty paragraph
6023 if (!this.opts
.keepInlineOnEnter
) {
6024 setTimeout($.proxy(function () {
6025 var inline
= this.selection
.inline();
6026 if (inline
&& this.utils
.isEmpty(inline
.innerHTML
)) {
6027 var parent
= this.selection
.block();
6029 //this.caret.start(parent);
6031 var range
= document
.createRange();
6032 range
.setStart(parent
, 0);
6034 var textNode
= document
.createTextNode('\u200B');
6036 range
.insertNode(textNode
);
6037 range
.setStartAfter(textNode
);
6038 range
.collapse(true);
6040 var sel
= window
.getSelection();
6041 sel
.removeAllRanges();
6042 sel
.addRange(range
);
6048 checkEvents: function (arrow
, key
) {
6049 if (!arrow
&& (this.core
.getEvent() === 'click' || this.core
.getEvent() === 'arrow')) {
6050 this.core
.addEvent(false);
6052 if (this.keydown
.checkKeyEvents(key
)) {
6057 checkKeyEvents: function (key
) {
6058 var k
= this.keyCode
;
6071 return ($.inArray(key
, keys
) === -1) ? true : false;
6074 addArrowsEvent: function (arrow
) {
6079 if ((this.core
.getEvent() === 'click' || this.core
.getEvent() === 'arrow')) {
6080 this.core
.addEvent(false);
6084 this.core
.addEvent('arrow');
6086 setupBuffer: function (e
, key
) {
6087 if (this.keydown
.ctrl
&& key
=== 90 && !e
.shiftKey
&& !e
.altKey
&& this.sBuffer
.length
) // z key
6094 else if (this.keydown
.ctrl
&& key
=== 90 && e
.shiftKey
&& !e
.altKey
&& this.sRebuffer
.length
!== 0) {
6099 else if (!this.keydown
.ctrl
) {
6100 if (key
=== this.keyCode
.SPACE
|| key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
|| (key
=== this.keyCode
.ENTER
&& !e
.ctrlKey
&& !e
.shiftKey
)) {
6105 exitFromBlockquote: function (e
) {
6106 if (!this.utils
.isEndOfElement(this.keydown
.blockquote
)) {
6110 var tmp
= this.clean
.removeSpacesHard($(this.keydown
.blockquote
).html());
6111 if (tmp
.search(/(<br\s?\/?>){1}$/i) !== -1) {
6114 var $last
= $(this.keydown
.blockquote
).children().last();
6116 $last
.filter('br').remove();
6117 $(this.keydown
.blockquote
).children().last().filter('span').remove();
6119 var node
= $(this.opts
.emptyHtml
);
6120 $(this.keydown
.blockquote
).after(node
);
6121 this.caret
.start(node
);
6129 onArrowDown: function () {
6130 var tags
= [this.keydown
.blockquote
, this.keydown
.pre
, this.keydown
.figcaption
];
6132 for (var i
= 0; i
< tags
.length
; i
++) {
6134 this.keydown
.insertAfterLastElement(tags
[i
]);
6139 onArrowUp: function () {
6140 var tags
= [this.keydown
.blockquote
, this.keydown
.pre
, this.keydown
.figcaption
];
6142 for (var i
= 0; i
< tags
.length
; i
++) {
6144 this.keydown
.insertBeforeFirstElement(tags
[i
]);
6149 insertAfterLastElement: function (element
) {
6150 if (!this.utils
.isEndOfElement(element
)) {
6154 var last
= this.core
.editor().contents().last();
6155 var $next
= (element
.tagName
=== 'FIGCAPTION') ? $(this.keydown
.block
).parent().next() : $(
6156 this.keydown
.block
).next();
6158 if ($next
.length
!== 0) {
6161 else if (last
.length
=== 0 && last
[0] !== element
) {
6162 this.caret
.start(last
);
6166 var node
= $(this.opts
.emptyHtml
);
6168 if (element
.tagName
=== 'FIGCAPTION') {
6169 $(element
).parent().after(node
);
6172 $(element
).after(node
);
6175 this.caret
.start(node
);
6179 insertBeforeFirstElement: function (element
) {
6180 if (!this.utils
.isStartOfElement()) {
6184 if (this.core
.editor().contents().length
> 1 && this.core
.editor().contents().first()[0] !== element
) {
6188 var node
= $(this.opts
.emptyHtml
);
6189 $(element
).before(node
);
6190 this.caret
.start(node
);
6193 onTab: function (e
, key
) {
6194 if (!this.opts
.tabKey
) {
6198 var isList
= (this.keydown
.block
&& this.keydown
.block
.tagName
=== 'LI');
6199 if (this.utils
.isEmpty(this.code
.get()) || (!isList
&& !this.keydown
.pre
&& this.opts
.tabAsSpaces
=== false)) {
6206 var isListStart
= (isList
&& this.utils
.isStartOfElement(this.keydown
.block
));
6209 if (this.keydown
.pre
&& !e
.shiftKey
) {
6210 node
= (this.opts
.preSpaces
) ? document
.createTextNode(Array(this.opts
.preSpaces
+ 1).join(
6211 '\u00a0')) : document
.createTextNode('\t');
6212 this.insert
.node(node
);
6214 else if (this.opts
.tabAsSpaces
!== false && !isListStart
) {
6215 node
= document
.createTextNode(Array(this.opts
.tabAsSpaces
+ 1).join(
6217 this.insert
.node(node
);
6220 if (e
.metaKey
&& key
=== 219) {
6221 this.indent
.decrease();
6223 else if (e
.metaKey
&& key
=== 221) {
6224 this.indent
.increase();
6226 else if (!e
.shiftKey
) {
6227 this.indent
.increase();
6230 this.indent
.decrease();
6236 setupSelectAll: function (e
, key
) {
6237 if (this.keydown
.ctrl
&& key
=== 65) {
6238 this.utils
.enableSelectAll();
6240 else if (key
!== this.keyCode
.LEFT_WIN
&& !this.keydown
.ctrl
) {
6241 this.utils
.disableSelectAll();
6244 insertNewLine: function (e
) {
6247 var node
= document
.createTextNode('\n');
6249 var sel
= this.selection
.get();
6250 var range
= this.selection
.range(sel
);
6252 range
.deleteContents();
6253 range
.insertNode(node
);
6255 this.caret
.after(node
);
6259 insertParagraph: function (e
) {
6262 var p
= document
.createElement('p');
6263 //p.innerHTML = this.opts.invisibleSpace;
6264 p
.innerHTML
= '<br>';
6266 var sel
= this.selection
.get();
6267 var range
= this.selection
.range(sel
);
6269 range
.deleteContents();
6270 range
.insertNode(p
);
6272 this.caret
.start(p
);
6276 insertBreakLine: function (e
) {
6277 return this.keydown
.insertBreakLineProcessing(e
);
6279 insertDblBreakLine: function (e
) {
6280 return this.keydown
.insertBreakLineProcessing(e
, true);
6282 insertBreakLineProcessing: function (e
, dbl
) {
6283 e
.stopPropagation();
6285 var br1
= document
.createElement('br');
6286 this.insert
.node(br1
);
6289 var br2
= document
.createElement('br');
6290 this.insert
.node(br2
);
6291 this.caret
.after(br2
);
6294 this.caret
.after(br1
);
6300 wrapToParagraph: function () {
6301 var $current
= $(this.keydown
.current
);
6302 var node
= $('<p>').append($current
.clone());
6303 $current
.replaceWith(node
);
6305 var next
= $(node
).next();
6306 if (typeof (next
[0]) !== 'undefined' && next
[0].tagName
=== 'BR') {
6310 this.caret
.end(node
);
6313 replaceToParagraph: function (tag
) {
6314 var blockElem
= this.selection
.block();
6315 var $prev
= $(blockElem
).prev();
6317 var blockHtml
= blockElem
.innerHTML
.replace(/<br\s?\/?>/gi, '');
6318 if (blockElem
.tagName
=== tag
&& this.utils
.isEmpty(blockHtml
) && !$(blockElem
).hasClass(
6320 var p
= document
.createElement('p');
6321 $(blockElem
).replaceWith(p
);
6323 this.keydown
.setCaretToParagraph(p
);
6327 else if (blockElem
.tagName
=== 'P') {
6328 $(blockElem
).removeAttr('class').removeAttr('style');
6331 if (this.detect
.isIe() && this.utils
.isEmpty(blockHtml
) && this.utils
.isInline(
6332 this.keydown
.parent
)) {
6333 $(blockElem
).on('input', $.proxy(function () {
6334 var parent
= this.selection
.parent();
6335 if (this.utils
.isInline(parent
)) {
6336 var html
= $(parent
).html();
6337 $(blockElem
).html(html
);
6338 this.caret
.end(blockElem
);
6341 $(blockElem
).off('keyup');
6348 else if ($prev
.hasClass(this.opts
.videoContainerClass
)) {
6349 $prev
.removeAttr('class');
6351 var p
= document
.createElement('p');
6352 $prev
.replaceWith(p
);
6354 this.keydown
.setCaretToParagraph(p
);
6359 setCaretToParagraph: function (p
) {
6360 var range
= document
.createRange();
6361 range
.setStart(p
, 0);
6363 var textNode
= document
.createTextNode('\u200B');
6365 range
.insertNode(textNode
);
6366 range
.setStartAfter(textNode
);
6367 range
.collapse(true);
6369 var sel
= window
.getSelection();
6370 sel
.removeAllRanges();
6371 sel
.addRange(range
);
6373 removeInvisibleSpace: function () {
6374 var $current
= $(this.keydown
.current
);
6375 if ($current
.text().search(/^\u200B$/g) === 0) {
6379 removeEmptyListInTable: function (e
) {
6380 var $current
= $(this.keydown
.current
);
6381 var $parent
= $(this.keydown
.parent
);
6382 var td
= $current
.closest('td', this.$editor
[0]);
6384 if (td
.length
!== 0 && $current
.closest('li',
6386 ) && $parent
.children('li').length
=== 1) {
6387 if (!this.utils
.isEmpty($current
.text())) {
6396 this.caret
.start(td
);
6399 removeEmptyLists: function () {
6400 var removeIt = function () {
6401 var html
= $.trim(this.innerHTML
).replace(/\/t\/n/g, '');
6407 this.core
.editor().find('li').each(removeIt
);
6408 this.core
.editor().find('ul, ol').each(removeIt
);
6410 formatEmpty: function (e
) {
6411 var html
= $.trim(this.core
.editor().html());
6413 if (!this.utils
.isEmpty(html
)) {
6419 if (this.opts
.type
=== 'inline' || this.opts
.type
=== 'pre') {
6420 this.core
.editor().html(this.marker
.html());
6421 this.selection
.restore();
6424 var updateHtml = function() {
6425 this.core
.editor().html(this.opts
.emptyHtml
);
6429 if (Environment
!== null && Environment
.platform() === 'ios') {
6430 // In iOS Safari the backspace sometimes appears to be triggered twice if the editor
6431 // is completely empty. After debugging for way too much time, and realizing that
6432 // the remote debugger's breakpoints alter the behavior of async callbacks (*), this
6433 // should solve the issue.
6435 // (*) Set up a `console.log()` inside a MutationObserver and then make use of the
6436 // `debugger;` statement to halt the execution flow. The observer is executed, but
6437 // the output never appears on the console. Output works if there is no breakpoint.
6438 setTimeout(updateHtml
, 50);
6452 keyup: function () {
6454 init: function (e
) {
6455 if (this.rtePaste
) {
6460 this.keyup
.block
= this.selection
.block();
6461 this.keyup
.current
= this.selection
.current();
6462 this.keyup
.parent
= this.selection
.parent();
6463 this.keyup
.lastShiftKey
= e
.shiftKey
;
6466 var stop
= this.core
.callback('keyup', e
);
6467 if (stop
=== false) {
6472 // replace a prev figure to paragraph if caret is before image
6473 if (key
=== this.keyCode
.ENTER
) {
6474 if (this.keyup
.block
&& this.keyup
.block
.tagName
=== 'FIGURE') {
6475 var $prev
= $(this.keyup
.block
).prev();
6476 if ($prev
.length
!== 0 && $prev
[0].tagName
=== 'FIGURE') {
6477 var $newTag
= this.utils
.replaceToTag($prev
, 'p');
6478 this.caret
.start($newTag
);
6484 // replace figure to paragraph
6485 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
6486 if (this.utils
.isSelectAll()) {
6492 // if caret before figure - delete image
6493 if (this.keyup
.block
&& this.keydown
.block
&& this.keyup
.block
.tagName
=== 'FIGURE' && this.utils
.isStartOfElement(
6494 this.keydown
.block
)) {
6497 this.selection
.save();
6498 $(this.keyup
.block
).find('figcaption').remove();
6499 $(this.keyup
.block
).find('img').first().remove();
6500 this.utils
.replaceToTag(this.keyup
.block
, 'p');
6502 var $marker
= this.marker
.find();
6503 $('html, body').animate({scrollTop
: $marker
.position().top
+ 20},
6507 this.selection
.restore();
6511 // if paragraph does contain only image replace to figure
6512 if (this.keyup
.block
&& this.keyup
.block
.tagName
=== 'P') {
6513 var isContainImage
= $(this.keyup
.block
).find('img').length
;
6514 var text
= $(this.keyup
.block
).text().replace(/\u200B/g, '');
6515 if (text
=== '' && isContainImage
!== 0) {
6516 this.utils
.replaceToTag(this.keyup
.block
, 'figure');
6520 // if figure does not contain image - replace to paragraph
6521 if (this.keyup
.block
&& this.keyup
.block
.tagName
=== 'FIGURE' && $(this.keyup
.block
).find(
6522 'img').length
=== 0) {
6523 this.selection
.save();
6524 this.utils
.replaceToTag(this.keyup
.block
, 'p');
6525 this.selection
.restore();
6537 this.opts
.curLang
= this.opts
.langs
[this.opts
.lang
];
6539 get: function (name
) {
6540 return (typeof this.opts
.curLang
[name
] !== 'undefined') ? this.opts
.curLang
[name
] : '';
6548 insert: function () {
6552 this.insert
.html(this.line
.getLineHtml());
6555 var $hr
= this.core
.editor().find('#redactor-hr-tmp-id');
6556 $hr
.removeAttr('id');
6558 this.core
.callback('insertedLine', $hr
);
6562 getLineHtml: function () {
6563 var html
= '<hr id="redactor-hr-tmp-id" />';
6564 if (!this.detect
.isFirefox() && this.utils
.isEmpty()) {
6565 html
+= '<p>' + this.opts
.emptyHtml
+ '</p>';
6570 removeOnBackspace: function (e
) {
6571 if (!this.utils
.isCollapsed()) {
6575 var $block
= $(this.selection
.block());
6576 if ($block
.length
=== 0 || !this.utils
.isStartOfElement($block
)) {
6580 // if hr is previous element
6581 var $prev
= $block
.prev();
6582 if ($prev
&& $prev
.length
!== 0 && $prev
[0].tagName
=== 'HR') {
6596 return $(this.selection
.inlines('a'));
6599 var nodes
= this.selection
.nodes();
6600 var $link
= $(this.selection
.current()).closest('a', this.core
.editor()[0]);
6602 return ($link
.length
=== 0 || nodes
.length
> 1) ? false : $link
;
6604 unlink: function (e
) {
6605 // if call from clickable element
6606 if (typeof e
!== 'undefined' && e
.preventDefault
) {
6613 var links
= this.selection
.inlines('a');
6614 if (links
.length
=== 0) {
6618 var $links
= this.link
.replaceLinksToText(links
);
6620 this.observe
.closeAllTooltip();
6621 this.core
.callback('deletedLink', $links
);
6624 insert: function (link
, cleaned
) {
6625 var $el
= this.link
.is();
6627 if (cleaned
!== true) {
6628 link
= this.link
.buildLinkFromObject($el
, link
);
6629 if (link
=== false) {
6638 link
= this.core
.callback('beforeInsertingLink', link
);
6640 if ($el
=== false) {
6643 $el
= this.link
.update($el
, link
);
6644 $el
= $(this.insert
.node($el
));
6646 var $parent
= $el
.parent();
6647 if (this.utils
.isRedactorParent($parent
) === false) {
6651 // remove unlink wrapper
6652 if ($parent
.hasClass('redactor-unlink')) {
6653 $parent
.replaceWith(function () {
6654 return $(this).contents();
6658 this.caret
.after($el
);
6659 this.core
.callback('insertedLink', $el
);
6663 $el
= this.link
.update($el
, link
);
6664 this.caret
.after($el
);
6670 update: function ($el
, link
) {
6671 $el
.text(link
.text
);
6672 $el
.attr('href', link
.url
);
6674 this.link
.target($el
, link
.target
);
6679 target: function ($el
, target
) {
6680 return (target
) ? $el
.attr('target', '_blank') : $el
.removeAttr('target');
6682 show: function (e
) {
6683 // if call from clickable element
6684 if (typeof e
!== 'undefined' && e
.preventDefault
) {
6689 this.observe
.closeAllTooltip();
6692 var $el
= this.link
.is();
6695 this.link
.buildModal($el
);
6698 var link
= this.link
.buildLinkFromElement($el
);
6700 // if link cut & paste inside editor browser added self host to a link
6701 link
.url
= this.link
.removeSelfHostFromUrl(link
.url
);
6704 if (this.opts
.linkNewTab
&& !$el
) {
6709 this.link
.setModalValues(link
);
6715 if (this.detect
.isDesktop()) {
6716 $('#redactor-link-url').focus();
6721 setModalValues: function (link
) {
6722 $('#redactor-link-blank').prop('checked', link
.target
);
6723 $('#redactor-link-url').val(link
.url
);
6724 $('#redactor-link-url-text').val(link
.text
);
6726 buildModal: function ($el
) {
6727 this.modal
.load('link',
6728 this.lang
.get(($el
=== false) ? 'link-insert' : 'link-edit'),
6733 var $btn
= this.modal
.getActionButton();
6734 $btn
.text(this.lang
.get(($el
=== false) ? 'insert' : 'save')).on('click',
6735 $.proxy(this.link
.callback
, this)
6739 callback: function () {
6741 var link
= this.link
.buildLinkFromModal();
6742 if (link
=== false) {
6750 this.link
.insert(link
, true);
6752 cleanUrl: function (url
) {
6753 return (typeof url
=== 'undefined') ? '' : $.trim(url
.replace(/[^\W\w\D\d+&\'@#/%?=~_
|!:,.;\(\)]/gi
,
6757 cleanText: function (text
) {
6758 return (typeof text
=== 'undefined') ? '' : $.trim(text
.replace(/(<([^>]+)>)/gi,
6762 getText: function (link
) {
6763 return (link
.text
=== '' && link
.url
!== '') ? this.link
.truncateUrl(link
.url
.replace(/<|>/g,
6767 isUrl: function (url
) {
6768 var reUrl
= new RegExp(
6769 '^((https?|ftp):\\/\\/)?(([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$',
6773 return (reUrl
.test(url
)) ? url
: false;
6775 isMailto: function (url
) {
6776 return (url
.search('@') !== -1 && /(http|ftp|https):\/\//i.test(url
) === false);
6778 isEmpty: function (link
) {
6779 return (link
.url
=== '' || (link
.text
=== '' && link
.url
=== ''));
6781 truncateUrl: function (url
) {
6782 return (url
.length
> this.opts
.linkSize
) ? url
.substring(0,
6786 parse: function (link
) {
6788 if (this.link
.isMailto(link
.url
)) {
6789 link
.url
= 'mailto:' + link
.url
.replace('mailto:', '');
6792 else if (link
.url
.search('#') !== 0) {
6793 if (this.opts
.linkValidation
) {
6794 link
.url
= (this.link
.isUrl(link
.url
)) ? 'http://' + link
.url
.replace(/(ftp
|https
?):\/\//gi,
6800 // empty url or text or isn't url
6801 return (this.link
.isEmpty(link
) || link
.url
=== false) ? false : link
;
6804 buildLinkFromModal: function () {
6808 link
.url
= this.link
.cleanUrl($('#redactor-link-url').val());
6811 link
.text
= this.link
.cleanText($('#redactor-link-url-text').val());
6812 link
.text
= this.link
.getText(link
);
6815 link
.target
= ($('#redactor-link-blank').prop('checked')) ? true : false;
6818 return this.link
.parse(link
);
6821 buildLinkFromObject: function ($el
, link
) {
6823 link
.url
= this.link
.cleanUrl(link
.url
);
6826 link
.text
= (typeof link
.text
=== 'undefined' && this.selection
.is()) ? this.selection
.text() : this.link
.cleanText(
6828 link
.text
= this.link
.getText(link
);
6831 link
.target
= ($el
=== false) ? link
.target
: this.link
.buildTarget($el
);
6834 return this.link
.parse(link
);
6837 buildLinkFromElement: function ($el
) {
6840 text
: (this.selection
.is()) ? this.selection
.text() : '',
6844 if ($el
!== false) {
6845 link
.url
= $el
.attr('href');
6846 link
.text
= $el
.text();
6847 link
.target
= this.link
.buildTarget($el
);
6852 buildTarget: function ($el
) {
6853 return (typeof $el
.attr('target') !== 'undefined' && $el
.attr('target') === '_blank') ? true : false;
6855 removeSelfHostFromUrl: function (url
) {
6856 var href
= self
.location
.href
.replace('#', '').replace(/\/$/i, '');
6857 return url
.replace(/^\/\#/, '#').replace(href
, '').replace('mailto:', '');
6859 replaceLinksToText: function (links
) {
6861 var $links
= $.each(links
, function (i
, s
) {
6863 var $unlinked
= $('<span class="redactor-unlink" />').append($el
.contents());
6864 $el
.replaceWith($unlinked
);
6873 // set caret after unlinked node
6874 if (links
.length
=== 1 && this.selection
.isCollapsed()) {
6875 this.caret
.after($first
);
6883 // =linkify -- UNSUPPORTED MODULE
6884 linkify: function () {
6886 isKey: function () {},
6887 isLink: function () {},
6888 isFiltered: function () {},
6889 handler: function () {},
6890 format: function () {},
6891 convertVideoLinks: function () {},
6892 convertImages: function () {},
6893 convertLinks: function () {}
6900 toggle: function (type
) {
6901 if (this.utils
.inBlocks(['table', 'td', 'th', 'tr'])) {
6905 type
= (type
=== 'orderedlist') ? 'ol' : type
;
6906 type
= (type
=== 'unorderedlist') ? 'ul' : type
;
6908 type
= type
.toLowerCase();
6911 this.selection
.save();
6913 var nodes
= this.list
._getBlocks();
6914 var block
= this.selection
.block();
6915 var $list
= $(block
).parents('ul, ol').last();
6916 if (nodes
.length
=== 0 && $list
.length
!== 0) {
6917 nodes
= [$list
.get(0)];
6920 nodes
= (this.list
._isUnformat(type
, nodes
)) ? this.list
._unformat(type
,
6922 ) : this.list
._format(type
, nodes
);
6924 this.selection
.restore();
6929 var current
= this.selection
.current();
6930 var $list
= $(current
).closest('ul, ol', this.core
.editor()[0]);
6932 return ($list
.length
=== 0) ? false : $list
;
6934 combineAfterAndBefore: function (block
) {
6935 var $prev
= $(block
).prev();
6936 var $next
= $(block
).next();
6937 var isEmptyBlock
= (block
&& block
.tagName
=== 'P' && (block
.innerHTML
=== '<br>' || block
.innerHTML
=== ''));
6938 var isBlockWrapped
= ($prev
.closest('ol, ul',
6939 this.core
.editor()[0]
6940 ).length
=== 1 && $next
.closest(
6942 this.core
.editor()[0]
6945 if (isEmptyBlock
&& isBlockWrapped
) {
6946 $prev
.children('li').last().append(this.marker
.get());
6947 $prev
.append($next
.contents());
6948 this.selection
.restore();
6956 _getBlocks: function () {
6957 var finalBlocks
= [];
6958 var blocks
= this.selection
.blocks();
6959 for (var i
= 0; i
< blocks
.length
; i
++) {
6960 var $el
= $(blocks
[i
]);
6961 var isFirst
= ($el
.parent().hasClass('redactor-in'));
6963 if (isFirst
) finalBlocks
.push(blocks
[i
]);
6968 _isUnformat: function (type
, nodes
) {
6970 for (var i
= 0; i
< nodes
.length
; i
++) {
6971 if (nodes
[i
].nodeType
!== 3) {
6972 var tag
= nodes
[i
].tagName
.toLowerCase();
6973 if (tag
=== type
|| tag
=== 'figure') {
6979 return (countLists
=== nodes
.length
);
6981 _uniteBlocks: function (nodes
, tags
) {
6983 var blocks
= {0: []};
6984 var lastcell
= false;
6985 for (var i
= 0; i
< nodes
.length
; i
++) {
6986 var $node
= $(nodes
[i
]);
6987 var $cell
= $node
.closest('th, td');
6989 if ($cell
.length
!== 0) {
6990 if ($cell
.get(0) !== lastcell
) {
6996 if (this.list
._isUniteBlock(nodes
[i
], tags
)) {
6997 blocks
[z
].push(nodes
[i
]);
7001 if (this.list
._isUniteBlock(nodes
[i
], tags
)) {
7002 blocks
[z
].push(nodes
[i
]);
7011 lastcell
= $cell
.get();
7016 _isUniteBlock: function (node
, tags
) {
7017 return (node
.nodeType
=== 3 || tags
.indexOf(node
.tagName
.toLowerCase()) !== -1);
7019 _createList: function (type
, blocks
, key
) {
7020 var last
= blocks
[blocks
.length
- 1];
7021 var $last
= $(last
);
7022 var $list
= $('<' + type
+ '>');
7027 _createListItem: function (item
) {
7028 var $item
= $('<li>');
7029 if (item
.nodeType
=== 3) {
7034 $item
.append($el
.contents());
7040 _format: function (type
, nodes
) {
7055 var blocks
= this.list
._uniteBlocks(nodes
, tags
);
7058 for (var key
in blocks
) {
7059 var items
= blocks
[key
];
7060 var $list
= this.list
._createList(type
, blocks
[key
]);
7062 for (var i
= 0; i
< items
.length
; i
++) {
7066 if (items
[i
].nodeType
!== 3 && (items
[i
].tagName
=== 'UL' || items
[i
].tagName
=== 'OL')) {
7067 $item
= $(items
[i
]).contents();
7068 $list
.append($item
);
7070 // other blocks or texts
7072 $item
= this.list
._createListItem(items
[i
]);
7073 //this.utils.normalizeTextNodes($item);
7074 $list
.append($item
);
7078 lists
.push($list
.get(0));
7083 _unformat: function (type
, nodes
) {
7085 if (nodes
.length
=== 1) {
7087 var $list
= $(nodes
[0]);
7088 var $items
= $list
.find('li');
7090 var selectedItems
= this.selection
.blocks(['li']);
7091 var block
= this.selection
.block();
7092 var $li
= $(block
).closest('li');
7093 if (selectedItems
.length
=== 0 && $li
.length
!== 0) {
7094 selectedItems
= [$li
.get(0)];
7098 if (selectedItems
.length
=== $items
.length
) {
7099 return this.list
._unformatEntire(nodes
[0]);
7102 var pos
= this.list
._getItemsPosition($items
, selectedItems
);
7105 if (pos
=== 'Top') {
7106 return this.list
._unformatAtSide('before',
7113 else if (pos
=== 'Bottom') {
7114 selectedItems
.reverse();
7115 return this.list
._unformatAtSide('after', selectedItems
, $list
);
7119 else if (pos
=== 'Middle') {
7120 var $last
= $(selectedItems
[selectedItems
.length
- 1]);
7124 var $parent
= false;
7125 var $secondList
= $('<' + $list
.get(0).tagName
.toLowerCase() + '>');
7126 $items
.each(function (i
, node
) {
7128 var $node
= $(node
);
7129 var $childList
= ($node
.children('ul, ol').length
!== 0);
7131 if ($node
.closest('.redactor-split-item').length
=== 0 && ($parent
=== false || $node
.closest(
7132 $parent
).length
=== 0)) {
7133 $node
.addClass('redactor-split-item');
7140 if (node
=== $last
.get(0)) {
7145 $items
.filter('.redactor-split-item').each(function (i
, node
) {
7146 var $node
= $(node
);
7147 $node
.removeClass('redactor-split-item');
7148 $secondList
.append(node
);
7151 $list
.after($secondList
);
7153 selectedItems
.reverse();
7154 for (var i
= 0; i
< selectedItems
.length
; i
++) {
7155 var $item
= $(selectedItems
[i
]);
7156 var $container
= this.list
._createUnformatContainer(
7159 $list
.after($container
);
7160 $container
.find('ul, ol').remove();
7170 for (var i
= 0; i
< nodes
.length
; i
++) {
7171 if (nodes
[i
].nodeType
!== 3 && nodes
[i
].tagName
.toLowerCase() === type
) {
7172 this.list
._unformatEntire(nodes
[i
]);
7177 _unformatEntire: function (list
) {
7178 var $list
= $(list
);
7179 var $items
= $list
.find('li');
7180 $items
.each(function (i
, node
) {
7181 var $item
= $(node
);
7182 var $container
= this.list
._createUnformatContainer($item
);
7185 $list
.before($container
);
7191 _unformatAtSide: function (type
, selectedItems
, $list
) {
7192 for (var i
= 0; i
< selectedItems
.length
; i
++) {
7193 var $item
= $(selectedItems
[i
]);
7194 var $container
= this.list
._createUnformatContainer($item
);
7196 $list
[type
]($container
);
7198 var $innerLists
= $container
.find('ul, ol').first();
7199 $item
.append($innerLists
);
7201 $innerLists
.each(function (i
, node
) {
7202 var $node
= $(node
);
7203 var $parent
= $node
.closest('li');
7205 if ($parent
.get(0) === selectedItems
[i
]) {
7207 $parent
.addClass('r-unwrapped');
7212 if (this.utils
.isEmpty($item
.html())) $item
.remove();
7216 $list
.find('.r-unwrapped').each(function (node
) {
7217 var $node
= $(node
);
7218 if ($node
.html().trim() === '') {
7222 $node
.removeClass('r-unwrapped');
7226 _getItemsPosition: function ($items
, selectedItems
) {
7229 var sFirst
= selectedItems
[0];
7230 var sLast
= selectedItems
[selectedItems
.length
- 1];
7232 var first
= $items
.first().get(0);
7233 var last
= $items
.last().get(0);
7235 if (first
=== sFirst
&& last
!== sLast
) {
7238 else if (first
!== sFirst
&& last
=== sLast
) {
7244 _createUnformatContainer: function ($item
) {
7245 var $container
= $('<p>');
7246 $container
.append($item
.contents());
7254 marker: function () {
7258 get: function (num
) {
7259 num
= (typeof num
=== 'undefined') ? 1 : num
;
7261 var marker
= document
.createElement('span');
7263 marker
.id
= 'selection-marker-' + num
;
7264 marker
.className
= 'redactor-selection-marker';
7265 marker
.innerHTML
= this.opts
.invisibleSpace
;
7269 html: function (num
) {
7270 return this.utils
.getOuterHtml(this.marker
.get(num
));
7272 find: function (num
) {
7273 num
= (typeof num
=== 'undefined') ? 1 : num
;
7275 return this.core
.editor().find('span#selection-marker-' + num
);
7277 insert: function () {
7278 var sel
= this.selection
.get();
7279 var range
= this.selection
.range(sel
);
7281 this.marker
.insertNode(range
, this.marker
.get(1), true);
7282 if (range
&& range
.collapsed
=== false) {
7283 this.marker
.insertNode(range
, this.marker
.get(2), false);
7287 remove: function () {
7288 this.core
.editor().find('.redactor-selection-marker').each(this.marker
.iterateRemove
);
7292 insertNode: function (range
, node
, collapse
) {
7293 var parent
= this.selection
.parent();
7294 if (range
=== null || $(parent
).closest('.redactor-in').length
=== 0) {
7298 range
= range
.cloneRange();
7301 range
.collapse(collapse
);
7302 range
.insertNode(node
);
7308 iterateRemove: function (i
, el
) {
7310 var text
= $el
.text().replace(/\u200B/g, '');
7311 var parent
= $el
.parent()[0];
7313 if (text
=== '') $el
.remove(); else $el
.replaceWith(function () { return $(this).contents(); });
7315 // if (parent && parent.normalize) parent.normalize();
7321 modal: function () {
7324 templates: function () {
7332 'link': String() + '<div class="redactor-modal-tab" data-title="General">' + '<section>' + '<label>URL</label>' + '<input type="url" id="redactor-link-url" aria-label="URL" />' + '</section>' + '<section>' + '<label>' + this.lang
.get(
7333 'text') + '</label>' + '<input type="text" id="redactor-link-url-text" aria-label="' + this.lang
.get(
7334 'text') + '" />' + '</section>' + '<section>' + '<label class="checkbox"><input type="checkbox" id="redactor-link-blank"> ' + this.lang
.get(
7335 'link-in-new-tab') + '</label>' + '</section>' + '<section>' + '<button id="redactor-modal-button-action">' + this.lang
.get(
7336 'insert') + '</button>' + '<button id="redactor-modal-button-cancel">' + this.lang
.get(
7337 'cancel') + '</button>' + '</section>' + '</div>'
7340 $.extend(this.opts
, this.opts
.modal
);
7343 addCallback: function (name
, callback
) {
7344 this.modal
.callbacks
[name
] = callback
;
7346 addTemplate: function (name
, template
) {
7347 this.opts
.modal
[name
] = template
;
7349 getTemplate: function (name
) {
7350 return this.opts
.modal
[name
];
7352 getModal: function () {
7353 return this.$modalBody
;
7355 getActionButton: function () {
7356 return this.$modalBody
.find('#redactor-modal-button-action');
7358 getCancelButton: function () {
7359 return this.$modalBody
.find('#redactor-modal-button-cancel');
7361 getDeleteButton: function () {
7362 return this.$modalBody
.find('#redactor-modal-button-delete');
7364 load: function () { /* WoltLabModal.js */ },
7365 show: function () { /* WoltLabModal.js */ },
7366 buildWidth: function () { },
7367 buildTabber: function () {},
7368 showTab: function () {},
7369 setTitle: function () { /* WoltLabModal.js */ },
7370 setContent: function () {
7371 this.$modalBody
.html(this.modal
.getTemplate(this.modal
.templateName
));
7373 this.modal
.getCancelButton().on('mousedown', $.proxy(this.modal
.close
, this));
7375 setDraggable: function () {},
7376 setEnter: function () {},
7377 build: function () {
7378 this.modal
.buildOverlay();
7380 this.$modalBox
= $('<div id="redactor-modal-box"/>').hide();
7381 this.$modal
= $('<div id="redactor-modal" role="dialog" />');
7382 this.$modalHeader
= $('<div id="redactor-modal-header" />');
7383 this.$modalClose
= $(
7384 '<button type="button" id="redactor-modal-close" aria-label="' + this.lang
.get(
7385 'close') + '" />').html('×');
7386 this.$modalBody
= $('<div id="redactor-modal-body" />');
7388 this.$modal
.append(this.$modalHeader
);
7389 this.$modal
.append(this.$modalBody
);
7390 this.$modal
.append(this.$modalClose
);
7391 this.$modalBox
.append(this.$modal
);
7392 this.$modalBox
.appendTo(document
.body
);
7395 buildOverlay: function () {
7396 this.$modalOverlay
= $('<div id="redactor-modal-overlay">').hide();
7397 $('body').prepend(this.$modalOverlay
);
7399 enableEvents: function () {},
7400 disableEvents: function () {},
7401 closeHandler: function () {},
7402 close: function () { /* WoltLabModal.js */ }
7407 observe: function () {
7410 if (typeof this.opts
.destroyed
!== 'undefined') {
7414 this.observe
.links();
7415 this.observe
.images();
7418 isCurrent: function ($el
, $current
) {
7419 if (typeof $current
=== 'undefined') {
7420 $current
= $(this.selection
.current());
7423 return $current
.is($el
) || $current
.parents($el
).length
> 0;
7425 toolbar: function () {
7426 this.observe
.buttons();
7427 this.observe
.dropdowns();
7429 buttons: function (e
, btnName
) {
7430 var current
= this.selection
.current();
7431 var parent
= this.selection
.parent();
7434 this.button
.setInactiveAll();
7437 this.button
.setInactiveAll(btnName
);
7440 if (e
=== false && btnName
!== 'html') {
7441 if ($.inArray(btnName
, this.opts
.activeButtons
) !== -1) {
7442 this.button
.toggleActive(btnName
);
7447 if (!this.utils
.isRedactorParent(current
)) {
7452 if (this.core
.editor().css('display') !== 'none') {
7453 if (this.utils
.isCurrentOrParentHeader() || this.utils
.isCurrentOrParent(
7454 ['table', 'pre', 'blockquote', 'li'])) {
7455 this.button
.disable('horizontalrule');
7458 this.button
.enable('horizontalrule');
7462 $.each(this.opts
.activeButtonsStates
, $.proxy(function (key
, value
) {
7463 var parentEl
= $(parent
).closest(key
, this.$editor
[0]);
7464 var currentEl
= $(current
).closest(key
, this.$editor
[0]);
7466 if (parentEl
.length
!== 0 && !this.utils
.isRedactorParent(parentEl
)) {
7470 if (!this.utils
.isRedactorParent(currentEl
)) {
7474 if (parentEl
.length
!== 0 || currentEl
.closest(key
,
7477 this.button
.setActive(value
);
7483 dropdowns: function () {
7484 var finded
= $('<div />').html(this.selection
.html()).find('a').length
;
7485 var $current
= $(this.selection
.current());
7486 var isRedactor
= this.utils
.isRedactorParent($current
);
7488 $.each(this.opts
.observe
.dropdowns
, $.proxy(function (key
, value
) {
7489 var observe
= value
.observe
, element
= observe
.element
,
7491 inValues
= typeof observe
['in'] !== 'undefined' ? observe
['in'] : false,
7492 outValues
= typeof observe
.out
!== 'undefined' ? observe
.out
: false;
7494 if (($current
.closest(element
).length
> 0 && isRedactor
) || (element
=== 'a' && finded
!== 0)) {
7495 this.observe
.setDropdownProperties($item
, inValues
, outValues
);
7498 this.observe
.setDropdownProperties($item
, outValues
, inValues
);
7503 setDropdownProperties: function ($item
, addProperties
, deleteProperties
) {
7504 if (deleteProperties
&& typeof deleteProperties
.attr
!== 'undefined') {
7505 this.observe
.setDropdownAttr($item
, deleteProperties
.attr
, true);
7508 if (typeof addProperties
.attr
!== 'undefined') {
7509 this.observe
.setDropdownAttr($item
, addProperties
.attr
);
7512 if (typeof addProperties
.title
!== 'undefined') {
7513 $item
.find('span').text(addProperties
.title
);
7516 setDropdownAttr: function ($item
, properties
, isDelete
) {
7517 $.each(properties
, function (key
, value
) {
7518 if (key
=== 'class') {
7520 $item
.addClass(value
);
7523 $item
.removeClass(value
);
7528 $item
.attr(key
, value
);
7531 $item
.removeAttr(key
);
7536 addDropdown: function ($item
, btnName
, btnObject
) {
7537 if (typeof btnObject
.observe
=== 'undefined') {
7541 btnObject
.item
= $item
;
7543 this.opts
.observe
.dropdowns
.push(btnObject
);
7545 images: function () {
7546 if (this.opts
.imageEditable
) {
7547 this.core
.editor().addClass('redactor-layer-img-edit');
7548 this.core
.editor().find('img').each($.proxy(function (i
, img
) {
7551 // IE fix (when we clicked on an image and then press backspace IE does goes to image's url)
7552 $img
.closest('a', this.$editor
[0]).on('click',
7553 function (e
) { e
.preventDefault(); }
7556 this.image
.setEditable($img
);
7561 links: function () {
7562 if (this.opts
.linkTooltip
) {
7563 this.core
.editor().find('a').each($.proxy(function (i
, s
) {
7565 if ($link
.data('cached') !== true) {
7566 $link
.data('cached', true);
7568 'touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7569 $.proxy(this.observe
.showTooltip
, this)
7576 getTooltipPosition: function ($link
) {
7577 return $link
.offset();
7579 showTooltip: function (e
) {
7580 var $el
= $(e
.target
);
7582 if ($el
[0].tagName
=== 'IMG') {
7586 if ($el
[0].tagName
!== 'A') {
7587 $el
= $el
.closest('a', this.$editor
[0]);
7590 if ($el
[0].tagName
!== 'A') {
7596 var pos
= this.observe
.getTooltipPosition($link
);
7597 var tooltip
= $('<span class="redactor-link-tooltip"></span>');
7599 var href
= $link
.attr('href');
7600 if (href
=== undefined) {
7604 if (href
.length
> 24) {
7605 href
= href
.substring(0, 24) + '...';
7608 var aLink
= $('<a href="' + $link
.attr('href') + '" target="_blank" />').html(
7609 href
).addClass('redactor-link-tooltip-action');
7610 var aEdit
= $('<a href="#" />').html(this.lang
.get('edit')).on('click',
7611 $.proxy(this.link
.show
, this)
7612 ).addClass('redactor-link-tooltip-action');
7613 var aUnlink
= $('<a href="#" />').html(this.lang
.get('unlink')).on('click',
7614 $.proxy(this.link
.unlink
, this)
7615 ).addClass('redactor-link-tooltip-action');
7617 tooltip
.append(aLink
).append(' | ').append(aEdit
).append(' | ').append(aUnlink
);
7619 var lineHeight
= parseInt($link
.css('line-height'), 10);
7620 var lineClicked
= Math
.ceil((e
.pageY
- pos
.top
) / lineHeight
);
7621 var top
= pos
.top
+ lineClicked
* lineHeight
;
7625 left
: pos
.left
+ 'px'
7628 $('.redactor-link-tooltip').remove();
7629 $('body').append(tooltip
);
7631 this.core
.editor().on('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7632 $.proxy(this.observe
.closeTooltip
, this)
7634 $(document
).on('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7635 $.proxy(this.observe
.closeTooltip
, this)
7638 closeAllTooltip: function () {
7639 $('.redactor-link-tooltip').remove();
7641 closeTooltip: function (e
) {
7642 e
= e
.originalEvent
|| e
;
7644 var target
= e
.target
;
7645 var $parent
= $(target
).closest('a', this.$editor
[0]);
7646 if ($parent
.length
!== 0 && $parent
[0].tagName
=== 'A' && target
.tagName
!== 'A') {
7649 else if ((target
.tagName
=== 'A' && this.utils
.isRedactorParent(target
)) || $(
7650 target
).hasClass('redactor-link-tooltip-action')) {
7654 this.observe
.closeAllTooltip();
7656 this.core
.editor().off('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7657 $.proxy(this.observe
.closeTooltip
, this)
7659 $(document
).off('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7660 $.proxy(this.observe
.closeTooltip
, this)
7668 offset: function () {
7670 get: function (node
) {
7671 var cloned
= this.offset
.clone(node
);
7672 if (cloned
=== false) {
7676 var div
= document
.createElement('div');
7677 div
.appendChild(cloned
.cloneContents());
7678 div
.innerHTML
= div
.innerHTML
.replace(/<img(.*?[^>])>$/gi, 'i');
7680 var text
= $.trim($(div
).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
7687 clone: function (node
) {
7688 var sel
= this.selection
.get();
7689 var range
= this.selection
.range(sel
);
7691 if (range
=== null && typeof node
=== 'undefined') {
7695 node
= (typeof node
=== 'undefined') ? this.$editor
: node
;
7696 if (node
=== false) {
7700 node
= node
[0] || node
;
7702 var cloned
= range
.cloneRange();
7703 cloned
.selectNodeContents(node
);
7704 cloned
.setEnd(range
.endContainer
, range
.endOffset
);
7708 set: function (start
, end
) {
7709 end
= (typeof end
=== 'undefined') ? start
: end
;
7711 if (!this.focus
.is()) {
7715 var sel
= this.selection
.get();
7716 var range
= this.selection
.range(sel
);
7717 var node
, offset
= 0;
7718 var walker
= document
.createTreeWalker(this.$editor
[0],
7719 NodeFilter
.SHOW_TEXT
,
7724 while ((node
= walker
.nextNode()) !== null) {
7725 offset
+= node
.nodeValue
.length
;
7726 if (offset
> start
) {
7727 range
.setStart(node
, node
.nodeValue
.length
+ start
- offset
);
7731 if (offset
>= end
) {
7732 range
.setEnd(node
, node
.nodeValue
.length
+ end
- offset
);
7737 range
.collapse(false);
7738 this.selection
.update(sel
, range
);
7744 paragraphize: function () {
7746 load: function (html
) {
7747 if (this.opts
.paragraphize
=== false || this.opts
.type
=== 'inline' || this.opts
.type
=== 'pre') {
7751 if (html
=== '' || html
=== '<p></p>') {
7752 return this.opts
.emptyHtml
;
7757 this.paragraphize
.safes
= [];
7758 this.paragraphize
.z
= 0;
7761 html
= html
.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>');
7762 html
= html
.replace(/<\/pre>/gi, '</pre>\n\n');
7763 html
= html
.replace(/<p>\s<br><\/p>/gi, '<p></p>');
7765 html
= this.paragraphize
.getSafes(html
);
7767 html
= html
.replace('<br>', '\n');
7768 html
= this.paragraphize
.convert(html
);
7770 html
= this.paragraphize
.clear(html
);
7771 html
= this.paragraphize
.restoreSafes(html
);
7774 html
= html
.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts
.paragraphizeBlocks
.join(
7775 '|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');
7777 return $.trim(html
);
7779 getSafes: function (html
) {
7780 var $div
= $('<div />').append(html
);
7782 // remove paragraphs in blockquotes
7783 $div
.find('blockquote p').replaceWith(function () {
7784 return $(this).append('<br />').contents();
7787 $div
.find(this.opts
.paragraphizeBlocks
.join(', ')).each($.proxy(function (i
, s
) {
7788 this.paragraphize
.z
++;
7789 this.paragraphize
.safes
[this.paragraphize
.z
] = s
.outerHTML
;
7791 return $(s
).replaceWith('\n#####replace' + this.paragraphize
.z
+ '#####\n\n');
7795 // deal with redactor selection markers
7796 $div
.find('span.redactor-selection-marker').each($.proxy(function (i
, s
) {
7797 this.paragraphize
.z
++;
7798 this.paragraphize
.safes
[this.paragraphize
.z
] = s
.outerHTML
;
7800 return $(s
).replaceWith('\n#####replace' + this.paragraphize
.z
+ '#####\n\n');
7805 restoreSafes: function (html
) {
7806 $.each(this.paragraphize
.safes
, function (i
, s
) {
7807 s
= (typeof s
!== 'undefined') ? s
.replace(/\$/g, '$') : s
;
7808 html
= html
.replace('#####replace' + i
+ '#####', s
);
7814 convert: function (html
) {
7815 html
= html
.replace(/\r\n/g, 'xparagraphmarkerz');
7816 html
= html
.replace(/\n/g, 'xparagraphmarkerz');
7817 html
= html
.replace(/\r/g, 'xparagraphmarkerz');
7820 html
= html
.replace(re1
, ' ');
7821 html
= $.trim(html
);
7823 var re2
= /xparagraphmarkerzxparagraphmarkerz/gi;
7824 html
= html
.replace(re2
, '</p><p>');
7826 var re3
= /xparagraphmarkerz/gi;
7827 html
= html
.replace(re3
, '<br>');
7829 html
= '<p>' + html
+ '</p>';
7831 html
= html
.replace('<p></p>', '');
7832 html
= html
.replace('\r\n\r\n', '');
7833 html
= html
.replace(/<\/p><p>/g, '</p>\r\n\r\n<p>');
7834 html
= html
.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7835 html
= html
.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7836 html
= html
.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7837 html
= html
.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7838 html
= html
.replace(/<p> <\/p>/gi, '');
7839 html
= html
.replace(/<p>\s?<br> <\/p>/gi, '');
7840 html
= html
.replace(/<p>\s?<br>/gi, '<p>');
7844 clear: function (html
) {
7846 html
= html
.replace(
7847 /<p>(.*?)#####replace(.*?)#####\s?<\/p>/gi,
7848 '<p>$1</p>#####replace$2#####'
7850 html
= html
.replace(/(<br\s?\/?>){2,}<\/p>/gi, '</p>');
7852 html
= html
.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>');
7853 html
= html
.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>');
7854 html
= html
.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>');
7855 html
= html
.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>');
7857 html
= html
.replace(new RegExp('<p><p ', 'gi'), '<p ');
7858 html
= html
.replace(new RegExp('<p><p>', 'gi'), '<p>');
7859 html
= html
.replace(new RegExp('</p></p>', 'gi'), '</p>');
7860 html
= html
.replace(new RegExp('<p>\\s?</p>', 'gi'), '');
7861 html
= html
.replace(new RegExp('\n</p>', 'gi'), '</p>');
7862 html
= html
.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>');
7863 html
= html
.replace(new RegExp('<p>\t*</p>', 'gi'), '');
7871 paste: function () {
7873 init: function (e
) {
7874 this.rtePaste
= true;
7875 var pre
= (this.opts
.type
=== 'pre' || this.utils
.isCurrentOrParent('pre')) ? true : false;
7878 if (this.detect
.isDesktop()) {
7880 if (!this.paste
.pre
&& this.opts
.clipboardImageUpload
&& this.opts
.imageUpload
&& this.paste
.detectClipboardUpload(
7882 if (this.detect
.isIe()) {
7883 setTimeout($.proxy(this.paste
.clipboardUpload
, this),
7892 this.utils
.saveScroll();
7893 this.selection
.save();
7894 this.paste
.createPasteBox(pre
);
7896 $(window
).on('scroll.redactor-freeze', $.proxy(function () {
7897 $(window
).scrollTop(this.saveBodyScroll
);
7901 setTimeout($.proxy(function () {
7902 var html
= this.paste
.getPasteBoxCode(pre
);
7906 this.selection
.restore();
7908 this.utils
.restoreScroll();
7911 var data
= this.clean
.getCurrentType(html
);
7914 html
= this.clean
.onPaste(html
, data
);
7917 var returned
= this.core
.callback('paste', html
);
7918 html
= (typeof returned
=== 'undefined') ? html
: returned
;
7920 this.paste
.insert(html
, data
);
7921 this.rtePaste
= false;
7923 // clean pre breaklines
7925 this.clean
.cleanPre();
7928 $(window
).off('scroll.redactor-freeze');
7933 getPasteBoxCode: function (pre
) {
7934 var html
= (pre
) ? this.$pasteBox
.val() : this.$pasteBox
.html();
7935 this.$pasteBox
.remove();
7939 createPasteBox: function (pre
) {
7947 this.$pasteBox
= (pre
) ? $('<textarea>').css(css
) : $('<div>').attr('contenteditable',
7950 this.paste
.appendPasteBox();
7951 this.$pasteBox
.focus();
7953 appendPasteBox: function () {
7954 if (this.detect
.isIe()) {
7955 this.core
.box().append(this.$pasteBox
);
7959 var $visibleModals
= $('.modal-body:visible');
7960 if ($visibleModals
.length
> 0) {
7961 $visibleModals
.append(this.$pasteBox
);
7964 $('body').prepend(this.$pasteBox
);
7968 detectClipboardUpload: function (e
) {
7969 e
= e
.originalEvent
|| e
;
7971 var clipboard
= e
.clipboardData
;
7972 if (this.detect
.isIe() || this.detect
.isFirefox()) {
7976 // prevent safari fake url
7977 var types
= clipboard
.types
;
7978 if (types
.indexOf('public.tiff') !== -1) {
7983 if (!clipboard
.items
|| !clipboard
.items
.length
) {
7987 var file
= clipboard
.items
[0].getAsFile();
7988 if (file
=== null) {
7992 var reader
= new FileReader();
7993 reader
.readAsDataURL(file
);
7994 reader
.onload
= $.proxy(this.paste
.insertFromClipboard
, this);
7998 clipboardUpload: function () {
7999 var imgs
= this.$editor
.find('img');
8000 $.each(imgs
, $.proxy(function (i
, s
) {
8001 if (s
.src
.search(/^data\:image/i) === -1) {
8005 var formData
= !!window
.FormData
? new FormData() : null;
8006 if (!window
.FormData
) {
8010 this.upload
.direct
= true;
8011 this.upload
.type
= 'image';
8012 this.upload
.url
= this.opts
.imageUpload
;
8013 this.upload
.callback
= $.proxy(function (data
) {
8014 if (this.detect
.isIe()) {
8015 $(s
).wrap($('<figure />'));
8019 var $parent
= $(s
).parent();
8020 this.utils
.replaceToTag($parent
, 'figure');
8024 this.core
.callback('imageUpload', $(s
), data
);
8028 var blob
= this.utils
.dataURItoBlob(s
.src
);
8030 formData
.append('clipboard', 1);
8031 formData
.append(this.opts
.imageUploadParam
, blob
);
8033 this.upload
.send(formData
, false);
8035 this.rtePaste
= false;
8039 insertFromClipboard: function (e
) {
8040 var formData
= !!window
.FormData
? new FormData() : null;
8041 if (!window
.FormData
) {
8045 this.upload
.direct
= true;
8046 this.upload
.type
= 'image';
8047 this.upload
.url
= this.opts
.imageUpload
;
8048 this.upload
.callback
= this.image
.insert
;
8050 var blob
= this.utils
.dataURItoBlob(e
.target
.result
);
8052 formData
.append('clipboard', 1);
8053 formData
.append(this.opts
.imageUploadParam
, blob
);
8055 this.upload
.send(formData
, e
);
8056 this.rtePaste
= false;
8058 insert: function (html
, data
) {
8060 this.insert
.raw(html
);
8062 else if (data
.text
) {
8063 this.insert
.text(html
);
8066 this.insert
.html(html
, data
);
8069 // Firefox Clipboard Observe
8070 if (this.detect
.isFirefox() && this.opts
.imageUpload
&& this.opts
.clipboardImageUpload
) {
8071 setTimeout($.proxy(this.paste
.clipboardUpload
, this), 100);
8078 // =placeholder -- UNSUPPORTED MODULE
8079 placeholder: function () {
8081 enable: function () {},
8082 show: function () {},
8083 update: function () {},
8084 hide: function () {},
8086 init: function () {},
8087 enabled: function () {},
8088 enableEvents: function () {},
8089 disableEvents: function () {},
8090 build: function () {},
8091 buildPosition: function () {},
8092 getPosition: function () {},
8093 isEditorEmpty: function () {},
8094 isAttr: function () {},
8095 destroy: function () {}
8099 // =progress -- UNSUPPORTED MODULE
8100 progress: function () {
8104 target
: document
.body
, // or id selector
8105 show: function () {},
8106 hide: function () {},
8107 update: function () {},
8109 build: function () {},
8110 destroy: function () {}
8115 selection: function () {
8118 if (window
.getSelection
) {
8119 return window
.getSelection();
8121 else if (document
.selection
&& document
.selection
.type
!== 'Control') {
8122 return document
.selection
;
8127 range: function (sel
) {
8128 if (typeof sel
=== 'undefined') {
8129 sel
= this.selection
.get();
8132 if (sel
.getRangeAt
&& sel
.rangeCount
) {
8133 return sel
.getRangeAt(0);
8139 return (this.selection
.isCollapsed()) ? false : true;
8141 isRedactor: function () {
8142 var range
= this.selection
.range();
8144 if (range
!== null) {
8145 var el
= range
.startContainer
.parentNode
;
8147 if ($(el
).hasClass('redactor-in') || $(el
).parents('.redactor-in').length
!== 0) {
8154 isCollapsed: function () {
8155 var sel
= this.selection
.get();
8157 return (sel
=== null) ? false : sel
.isCollapsed
;
8159 update: function (sel
, range
) {
8160 if (range
=== null) {
8164 sel
.removeAllRanges();
8165 sel
.addRange(range
);
8167 current: function () {
8168 var sel
= this.selection
.get();
8170 return (sel
=== null) ? false : sel
.anchorNode
;
8172 parent: function () {
8173 var current
= this.selection
.current();
8175 return (current
=== null) ? false : current
.parentNode
;
8177 block: function (node
) {
8178 node
= node
|| this.selection
.current();
8181 if (this.utils
.isBlockTag(node
.tagName
)) {
8182 return ($(node
).hasClass('redactor-in')) ? false : node
;
8185 node
= node
.parentNode
;
8190 inline: function (node
) {
8191 node
= node
|| this.selection
.current();
8194 if (this.utils
.isInlineTag(node
.tagName
)) {
8195 return ($(node
).hasClass('redactor-in')) ? false : node
;
8198 node
= node
.parentNode
;
8203 element: function (node
) {
8205 node
= this.selection
.current();
8209 if (node
.nodeType
=== 1) {
8210 if ($(node
).hasClass('redactor-in')) {
8217 node
= node
.parentNode
;
8223 var current
= this.selection
.current();
8225 return (current
=== null) ? false : this.selection
.current().previousSibling
;
8228 var current
= this.selection
.current();
8230 return (current
=== null) ? false : this.selection
.current().nextSibling
;
8232 blocks: function (tag
) {
8234 var nodes
= this.selection
.nodes(tag
);
8236 $.each(nodes
, $.proxy(function (i
, node
) {
8237 if (this.utils
.isBlock(node
)) {
8243 var block
= this.selection
.block();
8244 if (blocks
.length
=== 0 && block
=== false) {
8247 else if (blocks
.length
=== 0 && block
!== false) {
8255 inlines: function (tag
) {
8257 var nodes
= this.selection
.nodes(tag
);
8259 $.each(nodes
, $.proxy(function (i
, node
) {
8260 if (this.utils
.isInline(node
)) {
8266 var inline
= this.selection
.inline();
8267 if (inlines
.length
=== 0 && inline
=== false) {
8270 else if (inlines
.length
=== 0 && inline
!== false) {
8277 nodes: function (tag
) {
8278 var filter
= (typeof tag
=== 'undefined') ? [] : (($.isArray(tag
)) ? tag
: [tag
]);
8280 var sel
= this.selection
.get();
8281 var range
= this.selection
.range(sel
);
8283 var resultNodes
= [];
8285 if (this.utils
.isCollapsed()) {
8286 nodes
= [this.selection
.current()];
8289 var node
= range
.startContainer
;
8290 var endNode
= range
.endContainer
;
8293 if (node
=== endNode
) {
8298 while (node
&& node
!== endNode
) {
8299 nodes
.push(node
= this.selection
.nextNode(node
));
8302 // partially selected nodes
8303 node
= range
.startContainer
;
8304 while (node
&& node
!== range
.commonAncestorContainer
) {
8305 nodes
.unshift(node
);
8306 node
= node
.parentNode
;
8310 // remove service nodes
8311 $.each(nodes
, function (i
, s
) {
8313 var tagName
= (s
.nodeType
!== 1) ? false : s
.tagName
.toLowerCase();
8315 if ($(s
).hasClass('redactor-script-tag') || $(s
).hasClass(
8316 'redactor-selection-marker')) {
8319 else if (tagName
&& filter
.length
!== 0 && $.inArray(tagName
,
8325 resultNodes
.push(s
);
8330 return (resultNodes
.length
=== 0) ? [] : resultNodes
;
8332 nextNode: function (node
) {
8333 if (node
.hasChildNodes()) {
8334 return node
.firstChild
;
8337 while (node
&& !node
.nextSibling
) {
8338 node
= node
.parentNode
;
8345 return node
.nextSibling
;
8349 this.marker
.insert();
8350 this.savedSel
= this.core
.editor().html();
8352 restore: function (removeMarkers
) {
8353 var node1
= this.marker
.find(1);
8354 var node2
= this.marker
.find(2);
8356 if (this.detect
.isFirefox()) {
8357 this.core
.editor().focus();
8360 if (node1
.length
!== 0 && node2
.length
!== 0) {
8361 this.caret
.set(node1
, node2
);
8363 else if (node1
.length
!== 0) {
8364 this.caret
.start(node1
);
8367 this.core
.editor().focus();
8370 if (removeMarkers
!== false) {
8371 this.marker
.remove();
8372 this.savedSel
= false;
8375 saveInstant: function () {
8376 var el
= this.core
.editor()[0];
8377 var doc
= el
.ownerDocument
, win
= doc
.defaultView
;
8378 var sel
= win
.getSelection();
8380 if (!sel
.getRangeAt
|| !sel
.rangeCount
) {
8384 var range
= sel
.getRangeAt(0);
8385 var selectionRange
= range
.cloneRange();
8387 selectionRange
.selectNodeContents(el
);
8388 selectionRange
.setEnd(range
.startContainer
, range
.startOffset
);
8390 var start
= selectionRange
.toString().length
;
8394 end
: start
+ range
.toString().length
,
8395 node
: range
.startContainer
8400 restoreInstant: function (saved
) {
8401 if (typeof saved
=== 'undefined' && !this.saved
) {
8405 this.saved
= (typeof saved
!== 'undefined') ? saved
: this.saved
;
8407 var $node
= this.core
.editor().find(this.saved
.node
);
8408 if ($node
.length
!== 0 && $node
.text().trim().replace(/\u200B/g,
8412 var range
= document
.createRange();
8413 range
.setStart($node
[0], 0);
8415 var sel
= window
.getSelection();
8416 sel
.removeAllRanges();
8417 sel
.addRange(range
);
8424 var el
= this.core
.editor()[0];
8425 var doc
= el
.ownerDocument
, win
= doc
.defaultView
;
8426 var charIndex
= 0, range
= doc
.createRange();
8428 range
.setStart(el
, 0);
8429 range
.collapse(true);
8431 var nodeStack
= [el
], node
, foundStart
= false, stop
= false;
8432 while (!stop
&& (node
= nodeStack
.pop())) {
8433 if (node
.nodeType
== 3) {
8434 var nextCharIndex
= charIndex
+ node
.length
;
8435 if (!foundStart
&& this.saved
.start
>= charIndex
&& this.saved
.start
<= nextCharIndex
) {
8436 range
.setStart(node
, this.saved
.start
- charIndex
);
8440 if (foundStart
&& this.saved
.end
>= charIndex
&& this.saved
.end
<= nextCharIndex
) {
8441 range
.setEnd(node
, this.saved
.end
- charIndex
);
8444 charIndex
= nextCharIndex
;
8447 var i
= node
.childNodes
.length
;
8449 nodeStack
.push(node
.childNodes
[i
]);
8454 var sel
= win
.getSelection();
8455 sel
.removeAllRanges();
8456 sel
.addRange(range
);
8458 node: function (node
) {
8459 $(node
).prepend(this.marker
.get(1));
8460 $(node
).append(this.marker
.get(2));
8462 this.selection
.restore();
8465 this.core
.editor().focus();
8467 var sel
= this.selection
.get();
8468 var range
= this.selection
.range(sel
);
8470 range
.selectNodeContents(this.core
.editor()[0]);
8472 this.selection
.update(sel
, range
);
8474 remove: function () {
8475 this.selection
.get().removeAllRanges();
8477 replace: function (html
) {
8478 this.insert
.html(html
);
8481 return this.selection
.get().toString();
8485 var sel
= this.selection
.get();
8487 if (sel
.rangeCount
) {
8488 var container
= document
.createElement('div');
8489 var len
= sel
.rangeCount
;
8490 for (var i
= 0; i
< len
; ++i
) {
8491 container
.appendChild(sel
.getRangeAt(i
).cloneContents());
8494 html
= this.clean
.onGet(container
.innerHTML
);
8499 extractEndOfNode: function (node
) {
8500 var sel
= this.selection
.get();
8501 var range
= this.selection
.range(sel
);
8503 var clonedRange
= range
.cloneRange();
8504 clonedRange
.selectNodeContents(node
);
8505 clonedRange
.setStart(range
.endContainer
, range
.endOffset
);
8507 return clonedRange
.extractContents();
8511 removeMarkers: function () {
8512 this.marker
.remove();
8514 marker: function (num
) {
8515 return this.marker
.get(num
);
8517 markerHtml: function (num
) {
8518 return this.marker
.html(num
);
8525 shortcuts: function () {
8527 // based on https://github.com/jeresig/jquery.hotkeys
8528 hotkeysSpecialKeys
: {
8615 init: function (e
, key
) {
8616 // disable browser's hot keys for bold and italic if shortcuts off
8617 if (this.opts
.shortcuts
=== false) {
8618 if ((e
.ctrlKey
|| e
.metaKey
) && (key
=== 66 || key
=== 73)) {
8626 $.each(this.opts
.shortcuts
, $.proxy(function (str
, command
) {
8627 this.shortcuts
.build(e
, str
, command
);
8632 build: function (e
, str
, command
) {
8633 var handler
= $.proxy(function () {
8634 this.shortcuts
.buildHandler(command
);
8638 var keys
= str
.split(',');
8639 var len
= keys
.length
;
8640 for (var i
= 0; i
< len
; i
++) {
8641 if (typeof keys
[i
] === 'string') {
8642 this.shortcuts
.handler(e
, $.trim(keys
[i
]), handler
);
8647 buildHandler: function (command
) {
8649 if (command
.func
.search(/\./) !== '-1') {
8650 func
= command
.func
.split('.');
8651 if (typeof this[func
[0]] !== 'undefined') {
8652 this[func
[0]][func
[1]].apply(this, command
.params
);
8656 this[command
.func
].apply(this, command
.params
);
8659 handler: function (e
, keys
, origHandler
) {
8660 keys
= keys
.toLowerCase().split(' ');
8662 var special
= this.shortcuts
.hotkeysSpecialKeys
[e
.keyCode
];
8663 var character
= String
.fromCharCode(e
.which
).toLowerCase();
8664 var modif
= '', possible
= {};
8666 $.each(['alt', 'ctrl', 'meta', 'shift'], function (index
, specialKey
) {
8667 if (e
[specialKey
+ 'Key'] && special
!== specialKey
) {
8668 modif
+= specialKey
+ '+';
8673 possible
[modif
+ special
] = true;
8677 possible
[modif
+ character
] = true;
8678 possible
[modif
+ this.shortcuts
.hotkeysShiftNums
[character
]] = true;
8680 // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
8681 if (modif
=== 'shift+') {
8682 possible
[this.shortcuts
.hotkeysShiftNums
[character
]] = true;
8686 var len
= keys
.length
;
8687 for (var i
= 0; i
< len
; i
++) {
8688 if (possible
[keys
[i
]]) {
8690 return origHandler
.apply(this, arguments
);
8697 // =storage -- UNSUPPORTED MODULE
8698 storage: function () {
8701 add: function () {},
8702 status: function () {},
8703 observe: function () {},
8704 changes: function () {}
8710 toolbar: function () {
8712 build: function () {
8713 this.button
.hideButtons();
8714 this.button
.hideButtonsOnMobile();
8716 this.$toolbarBox
= $('<div class="redactor-toolbar-box" />');
8717 this.$toolbarBox
[0].innerHTML
= '<ul class="redactor-toolbar" id="redactor-toolbar-' + this.uuid
+ '" role="toolbar"></ul>';
8718 this.$toolbar
= $(this.$toolbarBox
[0].children
[0]);
8719 this.$box
[0].insertBefore(this.$toolbarBox
[0], this.$box
[0].firstChild
);
8721 this.button
.$toolbar
= this.$toolbar
;
8722 this.button
.setFormatting();
8723 this.button
.load(this.$toolbar
);
8725 require(['Core'], (function(Core
) {
8726 this.$toolbar
[0].addEventListener('keydown', this.toolbar
.keydown
.bind(this, Core
));
8729 createContainer: function () {},
8730 append: function () {},
8731 setOverflow: function () {},
8732 setFixed: function () {},
8733 setUnfixed: function () {},
8734 getBoxTop: function () {},
8735 observeScroll: function () {},
8736 observeScrollResize: function () {},
8737 observeScrollEnable: function () {},
8738 observeScrollDisable: function () {},
8739 setDropdownsFixed: function () {},
8740 unsetDropdownsFixed: function () {},
8741 setDropdownPosition: function () {},
8743 * @param {object} Core
8744 * @param {KeyboardEvent} event
8746 keydown: function(Core
, event
) {
8747 var activeButton
= document
.activeElement
;
8748 if (!activeButton
.classList
.contains('re-button')) {
8752 // Enter, Space, End, Home, ArrowLeft, ArrowRight, ArrowDown
8753 // Remarks: ArrowUp is not considered, because we do not support radio groups at the top level.
8754 var keyboardCodes
= [13, 32, 35, 36, 37, 39, 40];
8755 if (keyboardCodes
.indexOf(event
.which
) === -1) {
8759 // [Enter] || [Space]
8760 if (event
.which
=== 13 || event
.which
=== 32) {
8761 event
.preventDefault();
8763 require(['Core'], function(Core
) {
8764 Core
.triggerEvent(activeButton
, 'mousedown');
8770 // [ArrowDown] opens drop-down menus, but does nothing on "regular" buttons.
8771 if (event
.which
=== 40) {
8772 if (elAttr(activeButton
, 'aria-haspopup') !== 'true') {
8776 event
.preventDefault();
8777 Core
.triggerEvent(activeButton
, 'mousedown');
8779 var dropdown
= $(activeButton
).data('dropdown');
8780 var firstItem
= elBySel('li', dropdown
[0]);
8781 if (firstItem
) firstItem
.focus();
8785 event
.preventDefault();
8787 var buttons
= Array
.prototype.slice
.call(elBySelAll('.re-button', this.$toolbar
[0]));
8788 var newActiveButton
= null;
8790 if (event
.which
=== 35) {
8791 newActiveButton
= buttons
[buttons
.length
- 1];
8794 else if (event
.which
=== 36) {
8795 newActiveButton
= buttons
[0];
8798 var index
= buttons
.indexOf(activeButton
);
8801 if (event
.which
=== 37) {
8805 index
= buttons
.length
- 1;
8809 else if (event
.which
=== 39) {
8812 if (index
=== buttons
.length
) {
8817 newActiveButton
= buttons
[index
];
8820 if (newActiveButton
!== null) {
8821 newActiveButton
.focus();
8827 // =upload -- UNSUPPORTED MODULE
8828 upload: function () {
8830 init: function () {},
8831 directUpload: function () {},
8832 onDrop: function () {},
8833 traverseFile: function () {},
8834 setConfig: function () {},
8835 getType: function () {},
8836 getHiddenFields: function () {},
8837 send: function () {},
8838 onDrag: function () {},
8839 onDragLeave: function () {},
8840 clearImageFields: function () {},
8841 addImageFields: function () {},
8842 removeImageFields: function () {},
8843 clearFileFields: function () {},
8844 addFileFields: function () {},
8845 removeFileFields: function () {}
8849 // =s3 -- UNSUPPORTED MODULE
8850 uploads3: function () {
8852 send: function () {},
8853 executeOnSignedUrl: function () {},
8854 createCORSRequest: function () {},
8855 sendToS3: function () {}
8860 utils: function () {
8862 isEmpty: function (html
) {
8863 html
= (typeof html
=== 'undefined') ? this.core
.editor().html() : html
;
8865 html
= html
.replace(/[\u200B-\u200D\uFEFF]/g, '');
8866 html
= html
.replace(/ /gi, '');
8867 html
= html
.replace(/<\/?br\s?\/?>/g, '');
8868 html
= html
.replace(/\s/g, '');
8869 html
= html
.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
8870 html
= html
.replace(/<iframe(.*?[^>])>$/i, 'iframe');
8871 html
= html
.replace(/<source(.*?[^>])>$/i, 'source');
8873 // remove empty tags
8874 html
= html
.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8875 html
= html
.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8877 html
= $.trim(html
);
8881 isElement: function (obj
) {
8883 // Using W3 DOM2 (works for FF, Opera and Chrome)
8884 return obj
instanceof HTMLElement
;
8887 return (typeof obj
=== 'object') && (obj
.nodeType
=== 1) && (typeof obj
.style
=== 'object') && (typeof obj
.ownerDocument
=== 'object');
8890 strpos: function (haystack
, needle
, offset
) {
8891 var i
= haystack
.indexOf(needle
, offset
);
8892 return i
>= 0 ? i
: false;
8894 dataURItoBlob: function (dataURI
) {
8896 if (dataURI
.split(',')[0].indexOf('base64') >= 0) {
8897 byteString
= atob(dataURI
.split(',')[1]);
8900 byteString
= unescape(dataURI
.split(',')[1]);
8903 var mimeString
= dataURI
.split(',')[0].split(':')[1].split(';')[0];
8905 var ia
= new Uint8Array(byteString
.length
);
8906 for (var i
= 0; i
< byteString
.length
; i
++) {
8907 ia
[i
] = byteString
.charCodeAt(i
);
8910 return new Blob([ia
], {type
: mimeString
});
8912 getOuterHtml: function (el
) {
8913 return $('<div>').append($(el
).eq(0).clone()).html();
8915 cloneAttributes: function (from, to
) {
8916 from = from[0] || from;
8919 var attrs
= from.attributes
;
8920 var len
= attrs
.length
;
8922 var attr
= attrs
[len
];
8923 to
.attr(attr
.name
, attr
.value
);
8928 breakBlockTag: function () {
8929 var block
= this.selection
.block();
8934 var isEmpty
= this.utils
.isEmpty(block
.innerHTML
);
8936 var tag
= block
.tagName
.toLowerCase();
8937 if (tag
=== 'pre' || tag
=== 'li' || tag
=== 'td' || tag
=== 'th') {
8941 if (!isEmpty
&& this.utils
.isStartOfElement(block
)) {
8944 $next
: $(block
).next(),
8948 else if (!isEmpty
&& this.utils
.isEndOfElement(block
)) {
8951 $next
: $(block
).next(),
8956 var endOfNode
= this.selection
.extractEndOfNode(block
);
8957 var $nextPart
= $('<' + tag
+ ' />').append(endOfNode
);
8959 $nextPart
= this.utils
.cloneAttributes(block
, $nextPart
);
8960 $(block
).after($nextPart
);
8969 inBlocks: function (tags
) {
8970 tags
= ($.isArray(tags
)) ? tags
: [tags
];
8972 var blocks
= this.selection
.blocks();
8973 var len
= blocks
.length
;
8974 var contains
= false;
8975 for (var i
= 0; i
< len
; i
++) {
8976 if (blocks
[i
] !== false) {
8977 var tag
= blocks
[i
].tagName
.toLowerCase();
8979 if ($.inArray(tag
, tags
) !== -1) {
8988 inInlines: function (tags
) {
8989 tags
= ($.isArray(tags
)) ? tags
: [tags
];
8991 var inlines
= this.selection
.inlines();
8992 var len
= inlines
.length
;
8993 var contains
= false;
8994 for (var i
= 0; i
< len
; i
++) {
8995 var tag
= inlines
[i
].tagName
.toLowerCase();
8997 if ($.inArray(tag
, tags
) !== -1) {
9005 isTag: function (current
, tag
) {
9006 var element
= $(current
).closest(tag
, this.core
.editor()[0]);
9007 if (element
.length
=== 1) {
9013 isBlock: function (block
) {
9014 if (block
=== null) {
9018 block
= block
[0] || block
;
9020 return block
&& this.utils
.isBlockTag(block
.tagName
);
9022 isBlockTag: function (tag
) {
9023 return (typeof tag
=== 'undefined') ? false : this.reIsBlock
.test(tag
);
9025 isInline: function (inline
) {
9026 inline
= inline
[0] || inline
;
9028 return inline
&& this.utils
.isInlineTag(inline
.tagName
);
9030 isInlineTag: function (tag
) {
9031 return (typeof tag
=== 'undefined') ? false : this.reIsInline
.test(tag
);
9032 }, // parents detection
9033 isRedactorParent: function (el
) {
9038 if ($(el
).parents('.redactor-in').length
=== 0 || $(el
).hasClass('redactor-in')) {
9044 isCurrentOrParentHeader: function () {
9045 return this.utils
.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
9047 isCurrentOrParent: function (tagName
) {
9048 var parent
= this.selection
.parent();
9049 var current
= this.selection
.current();
9051 if ($.isArray(tagName
)) {
9053 $.each(tagName
, $.proxy(function (i
, s
) {
9054 if (this.utils
.isCurrentOrParentOne(current
, parent
, s
)) {
9059 return (matched
=== 0) ? false : true;
9062 return this.utils
.isCurrentOrParentOne(current
, parent
, tagName
);
9065 isCurrentOrParentOne: function (current
, parent
, tagName
) {
9066 tagName
= tagName
.toUpperCase();
9068 return parent
&& parent
.tagName
=== tagName
? parent
: current
&& current
.tagName
=== tagName
? current
: false;
9070 isEditorRelative: function () {
9071 var position
= this.core
.editor().css('position');
9072 var arr
= ['absolute', 'fixed', 'relative'];
9074 return ($.inArray(arr
, position
) !== -1);
9076 setEditorRelative: function () {
9077 this.core
.editor().addClass('redactor-relative');
9079 getScrollTarget: function () {
9080 var $scrollTarget
= $(this.opts
.scrollTarget
);
9082 return ($scrollTarget
.length
!== 0) ? $scrollTarget
: $(document
);
9084 freezeScroll: function () {
9085 this.freezeScrollTop
= this.utils
.getScrollTarget().scrollTop();
9086 this.utils
.getScrollTarget().scrollTop(this.freezeScrollTop
);
9088 unfreezeScroll: function () {
9089 if (typeof this.freezeScrollTop
=== 'undefined') {
9093 this.utils
.getScrollTarget().scrollTop(this.freezeScrollTop
);
9095 saveScroll: function () {
9096 this.tmpScrollTop
= this.utils
.getScrollTarget().scrollTop();
9098 restoreScroll: function () {
9099 if (typeof this.tmpScrollTop
=== 'undefined') {
9103 this.utils
.getScrollTarget().scrollTop(this.tmpScrollTop
);
9105 isStartOfElement: function (element
) {
9106 if (typeof element
=== 'undefined') {
9107 element
= this.selection
.block();
9113 return (this.offset
.get(element
) === 0) ? true : false;
9115 isEndOfElement: function (element
) {
9116 if (typeof element
=== 'undefined') {
9117 element
= this.selection
.block();
9123 var text
= $.trim($(element
).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
9126 var offset
= this.offset
.get(element
);
9128 return (offset
=== text
.length
) ? true : false;
9130 removeEmptyAttr: function (el
, attr
) {
9132 if (typeof $el
.attr(attr
) === 'undefined') {
9136 if ($el
.attr(attr
) === '') {
9137 $el
.removeAttr(attr
);
9143 replaceToTag: function (node
, tag
) {
9145 $(node
).replaceWith(function () {
9146 replacement
= $('<' + tag
+ ' />').append($(this).contents());
9148 for (var i
= 0; i
< this.attributes
.length
; i
++) {
9149 replacement
.attr(this.attributes
[i
].name
,
9150 this.attributes
[i
].value
9159 isSelectAll: function () {
9160 return this.selectAll
;
9162 enableSelectAll: function () {
9163 this.selectAll
= true;
9165 disableSelectAll: function () {
9166 this.selectAll
= false;
9168 disableBodyScroll: function () {},
9169 measureScrollbar: function () {
9170 var $body
= $('body');
9171 var scrollDiv
= document
.createElement('div');
9172 scrollDiv
.className
= 'redactor-scrollbar-measure';
9174 $body
.append(scrollDiv
);
9175 var scrollbarWidth
= scrollDiv
.offsetWidth
- scrollDiv
.clientWidth
;
9176 $body
[0].removeChild(scrollDiv
);
9177 return scrollbarWidth
;
9179 enableBodyScroll: function () {},
9180 appendFields: function (appendFields
, data
) {
9181 if (!appendFields
) {
9184 else if (typeof appendFields
=== 'object') {
9185 $.each(appendFields
, function (k
, v
) {
9186 if (v
!== null && v
.toString().indexOf('#') === 0) {
9197 var $fields
= $(appendFields
);
9198 if ($fields
.length
=== 0) {
9203 $fields
.each(function () {
9204 data
.append($(this).attr('name'), $(this).val());
9210 appendForms: function (appendForms
, data
) {
9215 var $forms
= $(appendForms
);
9216 if ($forms
.length
=== 0) {
9220 var formData
= $forms
.serializeArray();
9222 $.each(formData
, function (z
, f
) {
9223 data
.append(f
.name
, f
.value
);
9229 isRgb: function (str
) {
9230 return (str
.search(/^rgb/i) === 0);
9232 rgb2hex: function (rgb
) {
9234 /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
9236 return (rgb
&& rgb
.length
=== 4) ? '#' + ('0' + parseInt(rgb
[1],
9238 ).toString(16)).slice(-2) + ('0' + parseInt(rgb
[2],
9240 ).toString(16)).slice(-2) + ('0' + parseInt(rgb
[3],
9242 ).toString(16)).slice(-2) : '';
9246 isCollapsed: function () {
9247 return this.selection
.isCollapsed();
9249 isMobile: function () {
9250 return this.detect
.isMobile();
9252 isDesktop: function () {
9253 return this.detect
.isDesktop();
9255 isPad: function () {
9256 return this.detect
.isIpad();
9263 browser: function () {
9265 webkit: function () {
9266 return this.detect
.isWebkit();
9269 return this.detect
.isFirefox();
9272 return this.detect
.isIe();
9278 $(window
).on('load.tools.redactor', function () {
9279 $('[data-tools="redactor"]').redactor();
9283 Redactor
.prototype.init
.prototype = Redactor
.prototype;
9288 $.fn
.redactorAnimation = function (animation
, options
, callback
) {
9289 return this.each(function () {
9290 new redactorAnimation(this, animation
, options
, callback
);
9294 function redactorAnimation(element
, animation
, options
, callback
) {
9300 prefix
: 'redactor-',
9304 this.animation
= animation
;
9305 this.slide
= (this.animation
=== 'slideDown' || this.animation
=== 'slideUp');
9306 this.$element
= $(element
);
9307 this.prefixes
= ['', '-moz-', '-o-animation-', '-webkit-'];
9310 // options or callback
9311 if (typeof options
=== 'function') {
9316 this.opts
= $.extend(opts
, options
);
9321 this.$element
.height(this.$element
.height());
9325 this.init(callback
);
9329 redactorAnimation
.prototype = {
9331 init: function (callback
) {
9332 this.queue
.push(this.animation
);
9336 if (this.animation
=== 'show') {
9337 this.opts
.timing
= 'linear';
9338 this.$element
.removeClass('hide').show();
9340 if (typeof callback
=== 'function') {
9344 else if (this.animation
=== 'hide') {
9345 this.opts
.timing
= 'linear';
9346 this.$element
.hide();
9348 if (typeof callback
=== 'function') {
9353 this.animate(callback
);
9357 animate: function (callback
) {
9358 this.$element
.addClass('redactor-animated').css('display', '').removeClass('hide');
9359 this.$element
.addClass(this.opts
.prefix
+ this.queue
[0]);
9361 this.set(this.opts
.duration
+ 's', this.opts
.delay
+ 's', this.opts
.iterate
, this.opts
.timing
);
9363 var _callback
= (this.queue
.length
> 1) ? null : callback
;
9364 this.complete('AnimationEnd', $.proxy(function () {
9365 if (this.$element
.hasClass(this.opts
.prefix
+ this.queue
[0])) {
9369 if (this.queue
.length
) {
9370 this.animate(callback
);
9374 }, this), _callback
);
9376 set: function (duration
, delay
, iterate
, timing
) {
9377 var len
= this.prefixes
.length
;
9380 this.$element
.css(this.prefixes
[len
] + 'animation-duration', duration
);
9381 this.$element
.css(this.prefixes
[len
] + 'animation-delay', delay
);
9382 this.$element
.css(this.prefixes
[len
] + 'animation-iteration-count', iterate
);
9383 this.$element
.css(this.prefixes
[len
] + 'animation-timing-function', timing
);
9387 clean: function () {
9388 this.$element
.removeClass('redactor-animated');
9389 this.$element
.removeClass(this.opts
.prefix
+ this.queue
[0]);
9391 this.set('', '', '', '');
9394 complete: function (type
, make
, callback
) {
9395 this.$element
.one(type
.toLowerCase() + ' webkit' + type
+ ' o' + type
+ ' MS' + type
,
9396 $.proxy(function () {
9397 if (typeof make
=== 'function') {
9401 if (typeof callback
=== 'function') {
9414 if ($.inArray(this.animation
, effects
) !== -1) {
9415 this.$element
.css('display', 'none');
9420 this.$element
.css('height', '');