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) {
4730 this.selection.save();
4732 var nodes = this.inline.getClearedNodes();
4733 this.inline.setNodesStriked(nodes, tag, params);
4735 this.selection.restore();
4737 document.execCommand('strikethrough');
4739 this.selection.saveInstant();
4741 // WoltLab: Chrome misbehaves in some cases, causing the `<strike>` element for
4742 // contained elements to be stripped. Instead, those children are assigned the
4743 // CSS style `text-decoration-line: line-through`.
4744 var chromeElements = this.core.editor()[0].querySelectorAll('[style*="line
-through
"]'), element, strike;
4745 for (var i = 0, length = chromeElements.length; i < length; i++) {
4746 element = chromeElements[0];
4748 strike = document.createElement('strike');
4749 element.parentNode.insertBefore(strike, element);
4750 strike.appendChild(element);
4752 // Remove the bogus style attribute.
4753 element.style.removeProperty('text-decoration');
4757 this.core.editor().find('strike').each(function () {
4758 var $el = self.utils.replaceToTag(this, tag);
4759 self.inline.setParams($el[0], params);
4761 var $inside = $el.find(tag);
4762 var $parent = $el.parent();
4763 var $parentAround = $parent.parent();
4765 // revert formatting (safari bug)
4766 if ($parentAround.length !== 0 && $parentAround[0].tagName.toLowerCase() === tag && $parentAround.html() == $parent[0].outerHTML) {
4767 $el.replaceWith(function () { return $(this).contents(); });
4768 $parentAround.replaceWith(function () { return $(this).contents(); });
4774 if ($inside.length !== 0) {
4775 self.inline.cleanInsideOrParent($inside, params);
4779 if ($parent.html() == $el[0].outerHTML) {
4780 self.inline.cleanInsideOrParent($parent, params);
4783 // bugfix: remove empty inline tags after selection
4784 if (self.detect.isFirefox()) {
4785 self.core.editor().find(tag + ':empty').remove();
4789 this.selection.restoreInstant();
4791 cleanInsideOrParent: function ($el, params) {
4793 for (var key in params.data) {
4794 this.inline.removeSpecificAttr($el, key, params.data[key]);
4798 getClearedNodes: function () {
4799 var nodes = this.selection.nodes();
4801 var len = nodes.length;
4805 for (var i = 0; i < len; i++) {
4806 if ($(nodes[i]).hasClass('redactor-selection-marker')) {
4812 // find selected inline & text nodes
4813 for (var i = 0; i < len; i++) {
4814 if (i >= started && !this.utils.isBlockTag(nodes[i].tagName)) {
4815 newNodes.push(nodes[i]);
4821 isConvertableAttr: function (node, name, value) {
4822 var nodeAttrValue = $(node).attr(name);
4823 if (nodeAttrValue) {
4824 if (name === 'style') {
4825 value = $.trim(value).replace(/;$/, '');
4827 var rules = value.split(';');
4829 for (var i = 0; i < rules.length; i++) {
4830 var arr = rules[i].split(':');
4831 var ruleName = $.trim(arr[0]);
4832 var ruleValue = $.trim(arr[1]);
4834 if (ruleName.search(/color/) !== -1) {
4835 var val = $(node).css(ruleName);
4836 if (val && (val === ruleValue || this.utils.rgb2hex(
4837 val) === ruleValue)) {
4841 else if ($(node).css(ruleName) === ruleValue) {
4846 if (count === rules.length) {
4850 else if (nodeAttrValue === value) {
4858 isConvertable: function (node, nodeTag, tag, params) {
4859 if (nodeTag === tag) {
4862 for (var key in params.data) {
4863 count += this.inline.isConvertableAttr(node,
4869 if (count === Object.keys(params.data).length) {
4880 setNodesStriked: function (nodes, tag, params) {
4881 for (var i = 0; i < nodes.length; i++) {
4882 var nodeTag = (nodes[i].tagName) ? nodes[i].tagName.toLowerCase() : undefined;
4884 var parent = nodes[i].parentNode;
4885 var parentTag = (parent && parent.tagName) ? parent.tagName.toLowerCase() : undefined;
4887 var convertable = this.inline.isConvertable(parent,
4893 var $el = $(parent).replaceWith(function () {
4894 return $('<strike>').append($(this).contents());
4897 $el.attr('data-redactor-inline-converted');
4900 var convertable = this.inline.isConvertable(nodes[i],
4906 var $el = $(nodes[i]).replaceWith(function () {
4907 return $('<strike>').append($(this).contents());
4912 insertBreakpoint: function (inline, currentTag) {
4913 var breakpoint = document.createElement('span');
4914 breakpoint.id = 'redactor-inline-breakpoint';
4915 breakpoint = this.insert.node(breakpoint);
4917 var end = this.utils.isEndOfElement(inline);
4918 var code = this.utils.getOuterHtml(inline);
4919 var endTag = (end) ? '' : '<' + currentTag + '>';
4921 code = code.replace(/<span id="redactor
-inline
-breakpoint
"><\/span>/i,
4922 '</' + currentTag + '>' + endTag
4925 var $code = $(code);
4926 $(inline).replaceWith($code);
4928 if (endTag !== '') {
4929 this.utils.cloneAttributes(inline, $code.last());
4932 return $code.first();
4934 insertInline: function (tag) {
4935 var node = document.createElement(tag);
4937 this.insert.node(node);
4938 this.caret.start(node);
4942 arrangeTag: function (tag) {
4955 'strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub'
4958 tag = tag.toLowerCase();
4960 for (var i = 0; i < tags.length; i++) {
4961 if (tag === tags[i]) {
4968 getStyleParams: function (params) {
4970 var rules = params.trim().replace(/;$/, '').split(';');
4971 for (var i = 0; i < rules.length; i++) {
4972 var rule = rules[i].split(':');
4974 result[rule[0].trim()] = rule[1].trim();
4980 getParams: function (attr, value, type) {
4982 var func = 'toggle';
4983 if (typeof attr === 'object') {
4985 func = (value !== undefined) ? value : func;
4987 else if (attr !== undefined && value !== undefined) {
4990 func = (type !== undefined) ? type : func;
4998 setParams: function (node, params) {
5000 for (var key in params.data) {
5001 var $node = $(node);
5002 if (key === 'style') {
5003 node = this.inline[params.func + 'Style'](params.data[key],
5006 $node.attr('data-redactor-style-cache',
5010 else if (key === 'class') {
5011 node = this.inline[params.func + 'Class'](params.data[key],
5017 node = (params.func === 'remove') ? this.inline[params.func + 'Attr'](key,
5019 ) : this.inline[params.func + 'Attr'](key,
5025 if (key === 'style' && node.tagName === 'SPAN') {
5026 $node.attr('data-redactor-span', true);
5035 eachInline: function (node, callback) {
5037 var nodes = (node === undefined) ? this.selection.inlines() : [node];
5039 for (var i = 0; i < nodes.length; i++) {
5040 lastNode = callback(nodes[i])[0];
5048 replaceClass: function (value, node) {
5049 return this.inline.eachInline(node, function (el) {
5050 return $(el).removeAttr('class').addClass(value);
5053 toggleClass: function (value, node) {
5054 return this.inline.eachInline(node, function (el) {
5055 return $(el).toggleClass(value);
5058 addClass: function (value, node) {
5059 return this.inline.eachInline(node, function (el) {
5060 return $(el).addClass(value);
5063 removeClass: function (value, node) {
5064 return this.inline.eachInline(node, function (el) {
5065 return $(el).removeClass(value);
5068 removeAllClass: function (node) {
5069 return this.inline.eachInline(node, function (el) {
5070 return $(el).removeAttr('class');
5075 replaceAttr: function (name, value, node) {
5076 return this.inline.eachInline(node, function (el) {
5077 return $(el).removeAttr(name).attr(name.value);
5080 toggleAttr: function (name, value, node) {
5081 return this.inline.eachInline(node, function (el) {
5082 var attr = $(el).attr(name);
5084 return (attr) ? $(el).removeAttr(name) : $(el).attr(name.value);
5087 addAttr: function (name, value, node) {
5088 return this.inline.eachInline(node, function (el) {
5089 return $(el).attr(name, value);
5092 removeAttr: function (name, node) {
5093 return this.inline.eachInline(node, function (el) {
5096 $el.removeAttr(name);
5097 if (name === 'style') {
5098 $el.removeAttr('data-redactor-style-cache');
5104 removeAllAttr: function (node) {
5105 return this.inline.eachInline(node, function (el) {
5107 var len = el.attributes.length;
5108 for (var z = 0; z < len; z++) {
5109 $el.removeAttr(el.attributes[z].name);
5115 removeSpecificAttr: function (node, key, value) {
5117 if (key === 'style') {
5118 var arr = value.split(':');
5119 var name = arr[0].trim();
5122 if (this.utils.removeEmptyAttr(node, 'style')) {
5123 $el.removeAttr('data-redactor-style-cache');
5127 $el.removeAttr(key)[0];
5132 hasParentStyle: function ($el) {
5133 var $parent = $el.parent();
5135 return ($parent.length === 1 && $parent[0].tagName === $el[0].tagName && $parent.html() === $el[0].outerHTML) ? $parent : false;
5137 addParentStyle: function ($el) {
5138 var $parent = this.inline.hasParentStyle($el);
5140 var style = this.inline.getStyleParams($el.attr('style'));
5142 $parent.attr('data-redactor-style-cache', $parent.attr('style'));
5144 $el.replaceWith(function () {
5145 return $(this).contents();
5149 $el.attr('data-redactor-style-cache', $el.attr('style'));
5154 replaceStyle: function (params, node) {
5155 params = this.inline.getStyleParams(params);
5158 return this.inline.eachInline(node, function (el) {
5160 $el.removeAttr('style').css(params);
5162 var style = $el.attr('style');
5163 if (style) $el.attr('style', style.replace(/"/g
, '\''));
5165 $el
= self
.inline
.addParentStyle($el
);
5170 toggleStyle: function (params
, node
) {
5171 params
= this.inline
.getStyleParams(params
);
5174 return this.inline
.eachInline(node
, function (el
) {
5177 for (var key
in params
) {
5178 var newVal
= params
[key
];
5179 var oldVal
= $el
.css(key
);
5181 oldVal
= (self
.utils
.isRgb(oldVal
)) ? self
.utils
.rgb2hex(oldVal
) : oldVal
.replace(/"/g,
5184 newVal = (self.utils.isRgb(newVal)) ? self.utils.rgb2hex(newVal) : newVal.replace(/"/g
,
5188 if (oldVal
=== newVal
) {
5192 $el
.css(key
, newVal
);
5196 var style
= $el
.attr('style');
5197 if (style
) $el
.attr('style', style
.replace(/"/g, '\''));
5199 if (!self.utils.removeEmptyAttr(el, 'style')) {
5200 $el = self.inline.addParentStyle($el);
5203 $el.removeAttr('data-redactor-style-cache');
5209 addStyle: function (params, node) {
5210 params = this.inline.getStyleParams(params);
5213 return this.inline.eachInline(node, function (el) {
5218 var style = $el.attr('style');
5219 if (style) $el.attr('style', style.replace(/"/g
, '\''));
5221 $el
= self
.inline
.addParentStyle($el
);
5226 removeStyle: function (params
, node
) {
5227 params
= this.inline
.getStyleParams(params
);
5230 return this.inline
.eachInline(node
, function (el
) {
5233 for (var key
in params
) {
5237 if (self
.utils
.removeEmptyAttr(el
, 'style')) {
5238 $el
.removeAttr('data-redactor-style-cache');
5241 $el
.attr('data-redactor-style-cache', $el
.attr('style'));
5247 removeAllStyle: function (node
) {
5248 return this.inline
.eachInline(node
, function (el
) {
5249 return $(el
).removeAttr('style').removeAttr('data-redactor-style-cache');
5252 removeStyleRule: function (name
) {
5253 var parent
= this.selection
.parent();
5254 var nodes
= this.selection
.inlines();
5258 if (parent
&& parent
.tagName
=== 'SPAN') {
5259 this.inline
.removeStyleRuleAttr($(parent
), name
);
5262 for (var i
= 0; i
< nodes
.length
; i
++) {
5265 if ($.inArray(el
.tagName
.toLowerCase(),
5266 this.opts
.inlineTags
5267 ) != -1 && !$el
.hasClass('redactor-selection-marker')) {
5268 this.inline
.removeStyleRuleAttr($el
, name
);
5273 removeStyleRuleAttr: function ($el
, name
) {
5275 if (this.utils
.removeEmptyAttr($el
, 'style')) {
5276 $el
.removeAttr('data-redactor-style-cache');
5279 $el
.attr('data-redactor-style-cache', $el
.attr('style'));
5284 update: function (tag
, attr
, value
, type
) {
5285 tag
= this.inline
.arrangeTag(tag
);
5287 var params
= this.inline
.getParams(attr
, value
, type
);
5288 var nodes
= this.selection
.inlines();
5292 for (var i
= 0; i
< nodes
.length
; i
++) {
5294 if (tag
=== '*' || el
.tagName
.toLowerCase() === tag
) {
5295 result
.push(this.inline
.setParams(el
, params
));
5304 removeFormat: function () {
5305 this.selection
.save();
5307 var nodes
= this.inline
.getClearedNodes();
5308 for (var i
= 0; i
< nodes
.length
; i
++) {
5309 if (nodes
[i
].nodeType
=== 1) {
5310 $(nodes
[i
]).replaceWith(function () {
5311 return $(this).contents();
5316 this.selection
.restore();
5323 insert: function () {
5325 set: function (html
) {
5326 this.code
.set(html
);
5329 html: function (html
, data
) {
5330 this.core
.editor().focus();
5332 var block
= this.selection
.block();
5333 var inline
= this.selection
.inline();
5336 if (typeof data
=== 'undefined') {
5337 data
= this.clean
.getCurrentType(html
, true);
5338 html
= this.clean
.onPaste(html
, data
, true);
5341 html
= $.parseHTML(html
);
5344 var endNode
= $(html
).last();
5346 // delete selected content
5347 var sel
= this.selection
.get();
5348 var range
= this.selection
.range(sel
);
5349 range
.deleteContents();
5351 this.selection
.update(sel
, range
);
5353 // insert list in list
5355 var $list
= $(html
);
5356 if ($list
.length
!== 0 && ($list
[0].tagName
=== 'UL' || $list
[0].tagName
=== 'OL')) {
5358 this.insert
.appendLists(block
, $list
);
5363 if (data
.blocks
&& block
) {
5364 if (this.utils
.isSelectAll()) {
5365 this.core
.editor().html(html
);
5369 var breaked
= this.utils
.breakBlockTag();
5370 if (breaked
=== false) {
5371 this.insert
.placeHtml(html
);
5374 var $last
= $(html
).children().last();
5375 $last
.append(this.marker
.get());
5377 if (breaked
.type
=== 'start') {
5378 breaked
.$block
.before(html
);
5381 breaked
.$block
.after(html
);
5384 this.selection
.restore();
5385 this.core
.editor().find('p').each(function () {
5386 if ($.trim(this.innerHTML
) === '') {
5395 // remove same tag inside
5396 var $div
= $('<div/>').html(html
);
5397 $div
.find(inline
.tagName
.toLowerCase()).each(function () {
5398 $(this).contents().unwrap();
5402 html
= $.parseHTML(html
);
5404 endNode
= $(html
).last();
5408 if (this.utils
.isSelectAll()) {
5409 var $node
= $(this.opts
.emptyHtml
);
5410 this.core
.editor().html('').append($node
);
5412 this.caret
.end($node
);
5415 this.insert
.placeHtml(html
);
5419 this.utils
.disableSelectAll();
5421 if (data
.pre
) this.clean
.cleanPre();
5423 this.caret
.end(endNode
);
5425 text: function (text
) {
5426 text
= text
.toString();
5427 text
= $.trim(text
);
5429 var tmp
= document
.createElement('div');
5430 tmp
.innerHTML
= text
;
5431 text
= tmp
.textContent
|| tmp
.innerText
;
5433 if (typeof text
=== 'undefined') {
5437 this.core
.editor().focus();
5440 var blocks
= this.selection
.blocks();
5443 text
= text
.replace(/\n/g, ' ');
5446 if (this.utils
.isSelectAll()) {
5447 var $node
= $(this.opts
.emptyHtml
);
5448 this.core
.editor().html('').append($node
);
5450 this.caret
.end($node
);
5454 var sel
= this.selection
.get();
5455 var node
= document
.createTextNode(text
);
5457 if (sel
.getRangeAt
&& sel
.rangeCount
) {
5458 var range
= sel
.getRangeAt(0);
5459 range
.deleteContents();
5460 range
.insertNode(node
);
5461 range
.setStartAfter(node
);
5462 range
.collapse(true);
5464 this.selection
.update(sel
, range
);
5467 // wrap node if selected two or more block tags
5468 if (blocks
.length
> 1) {
5469 $(node
).wrap('<p>');
5470 this.caret
.after(node
);
5474 this.utils
.disableSelectAll();
5475 this.clean
.normalizeCurrentHeading();
5478 raw: function (html
) {
5479 this.core
.editor().focus();
5481 var sel
= this.selection
.get();
5483 var range
= this.selection
.range(sel
);
5484 range
.deleteContents();
5486 var el
= document
.createElement('div');
5487 el
.innerHTML
= html
;
5489 var frag
= document
.createDocumentFragment(), node
, lastNode
;
5490 while ((node
= el
.firstChild
)) {
5491 lastNode
= frag
.appendChild(node
);
5494 range
.insertNode(frag
);
5497 range
= range
.cloneRange();
5498 range
.setStartAfter(lastNode
);
5499 range
.collapse(true);
5500 sel
.removeAllRanges();
5501 sel
.addRange(range
);
5504 node: function (node
, deleteContent
) {
5505 if (typeof this.start
!== 'undefined') {
5506 this.core
.editor().focus();
5509 node
= node
[0] || node
;
5511 var block
= this.selection
.block();
5512 var gap
= this.utils
.isBlockTag(node
.tagName
);
5515 if (this.utils
.isSelectAll()) {
5517 this.core
.editor().html(node
);
5520 this.core
.editor().html($('<p>').html(node
));
5525 else if (gap
&& block
) {
5526 var breaked
= this.utils
.breakBlockTag();
5527 if (breaked
=== false) {
5528 this.insert
.placeNode(node
, deleteContent
);
5531 if (breaked
.type
=== 'start') {
5532 breaked
.$block
.before(node
);
5535 breaked
.$block
.after(node
);
5538 this.core
.editor().find('p:empty').remove();
5542 result
= this.insert
.placeNode(node
, deleteContent
);
5545 this.utils
.disableSelectAll();
5548 this.caret
.end(node
);
5554 appendLists: function (block
, $list
) {
5555 var $block
= $(block
);
5557 var isEmpty
= this.utils
.isEmpty(block
.innerHTML
);
5559 if (isEmpty
|| this.utils
.isEndOfElement(block
)) {
5561 $list
.find('li').each(function () {
5570 else if (this.utils
.isStartOfElement(block
)) {
5571 $list
.find('li').each(function () {
5572 $block
.before(this);
5577 var endOfNode
= this.selection
.extractEndOfNode(block
);
5579 $block
.after($('<li>').append(endOfNode
));
5580 $block
.append($list
);
5584 this.marker
.remove();
5587 this.caret
.end(last
);
5590 placeHtml: function (html
) {
5591 var marker
= document
.createElement('span');
5592 marker
.id
= 'redactor-insert-marker';
5593 marker
= this.insert
.node(marker
);
5595 $(marker
).before(html
);
5596 this.selection
.restore();
5597 this.caret
.after(marker
);
5600 placeNode: function (node
, deleteContent
) {
5601 var sel
= this.selection
.get();
5602 var range
= this.selection
.range(sel
);
5603 if (range
== null) {
5607 if (deleteContent
!== false) {
5608 range
.deleteContents();
5611 range
.insertNode(node
);
5612 range
.collapse(false);
5614 this.selection
.update(sel
, range
);
5616 nodeToPoint: function (e
, node
) {
5617 node
= node
[0] || node
;
5619 if (this.utils
.isEmpty()) {
5620 node
= (this.utils
.isBlock(node
)) ? node
: $('<p />').append(node
);
5622 this.core
.editor().html(node
);
5628 var x
= e
.clientX
, y
= e
.clientY
;
5629 if (document
.caretPositionFromPoint
) {
5630 var pos
= document
.caretPositionFromPoint(x
, y
);
5631 var sel
= document
.getSelection();
5632 range
= sel
.getRangeAt(0);
5633 range
.setStart(pos
.offsetNode
, pos
.offset
);
5634 range
.collapse(true);
5635 range
.insertNode(node
);
5637 else if (document
.caretRangeFromPoint
) {
5638 range
= document
.caretRangeFromPoint(x
, y
);
5639 range
.insertNode(node
);
5641 else if (typeof document
.body
.createTextRange
!== 'undefined') {
5642 range
= document
.body
.createTextRange();
5643 range
.moveToPoint(x
, y
);
5644 var endRange
= range
.duplicate();
5645 endRange
.moveToPoint(x
, y
);
5646 range
.setEndPoint('EndToEnd', endRange
);
5655 nodeToCaretPositionFromPoint: function (e
, node
) {
5656 this.insert
.nodeToPoint(e
, node
);
5658 marker: function () {
5659 this.marker
.insert();
5665 keydown: function () {
5667 init: function (e
) {
5668 if (this.rtePaste
) {
5673 var arrow
= (key
>= 37 && key
<= 40);
5675 this.keydown
.ctrl
= e
.ctrlKey
|| e
.metaKey
;
5676 this.keydown
.parent
= this.selection
.parent();
5677 this.keydown
.current
= this.selection
.current();
5678 this.keydown
.block
= this.selection
.block();
5681 this.keydown
.pre
= this.utils
.isTag(this.keydown
.current
, 'pre');
5682 this.keydown
.blockquote
= this.utils
.isTag(this.keydown
.current
, 'blockquote');
5683 this.keydown
.figcaption
= this.utils
.isTag(this.keydown
.current
, 'figcaption');
5684 this.keydown
.figure
= this.utils
.isTag(this.keydown
.current
, 'figure');
5687 var keydownStop
= this.core
.callback('keydown', e
);
5688 if (keydownStop
=== false) {
5694 this.shortcuts
.init(e
, key
);
5697 this.keydown
.checkEvents(arrow
, key
);
5698 this.keydown
.setupBuffer(e
, key
);
5700 if (this.utils
.isSelectAll() && (key
=== this.keyCode
.ENTER
|| key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
)) {
5703 this.code
.set(this.opts
.emptyHtml
);
5704 this.events
.changeHandler();
5708 this.keydown
.addArrowsEvent(arrow
);
5709 this.keydown
.setupSelectAll(e
, key
);
5711 // turn off enter key
5712 if (!this.opts
.enterKey
&& key
=== this.keyCode
.ENTER
) {
5716 var sel
= this.selection
.get();
5717 var range
= this.selection
.range(sel
);
5719 if (!range
.collapsed
) {
5720 range
.deleteContents();
5727 if (this.opts
.enterKey
&& key
=== this.keyCode
.DOWN
) {
5728 this.keydown
.onArrowDown();
5732 if (this.opts
.enterKey
&& key
=== this.keyCode
.UP
) {
5733 this.keydown
.onArrowUp();
5736 // replace to p before / after the table or into body
5737 if ((this.opts
.type
=== 'textarea' || this.opts
.type
=== 'div') && this.keydown
.current
&& this.keydown
.current
.nodeType
=== 3 && $(
5738 this.keydown
.parent
).hasClass('redactor-in')) {
5739 this.keydown
.wrapToParagraph();
5742 // on Shift+Space or Ctrl+Space
5743 if (!this.keyup
.lastShiftKey
&& key
=== this.keyCode
.SPACE
&& (e
.ctrlKey
|| e
.shiftKey
)) {
5746 return this.keydown
.onShiftSpace();
5749 // on Shift+Enter or Ctrl+Enter
5750 if (key
=== this.keyCode
.ENTER
&& (e
.ctrlKey
|| e
.shiftKey
)) {
5751 // iOS Safari will report the shift key to be pressed, if the caret is at the
5752 // front of the line and the next character should be an uppercase character.
5753 if (Environment
=== null || Environment
.platform() !== 'ios') {
5756 return this.keydown
.onShiftEnter(e
);
5761 if (key
=== this.keyCode
.ENTER
&& !e
.shiftKey
&& !e
.ctrlKey
&& !e
.metaKey
) {
5762 return this.keydown
.onEnter(e
);
5766 if (key
=== this.keyCode
.TAB
|| e
.metaKey
&& key
=== 221 || e
.metaKey
&& key
=== 219) {
5767 return this.keydown
.onTab(e
, key
);
5771 if (this.detect
.isFirefox() && key
=== this.keyCode
.BACKSPACE
&& this.keydown
.block
&& this.keydown
.block
.tagName
=== 'P' && this.utils
.isStartOfElement(
5772 this.keydown
.block
)) {
5773 var $prev
= $(this.keydown
.block
).prev();
5774 if ($prev
.length
!== 0) {
5777 $prev
.append(this.marker
.get());
5778 $prev
.append($(this.keydown
.block
).html());
5779 $(this.keydown
.block
).remove();
5781 this.selection
.restore();
5787 // backspace & delete
5788 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
5789 if (this.observe
.image
&& typeof this.observe
.image
!== 'undefined' && $(
5790 '#redactor-image-box').length
!== 0) {
5793 var $prev
= this.observe
.image
.closest('figure, p').prev();
5794 this.image
.remove(false);
5795 this.observe
.image
= false;
5797 if ($prev
&& $prev
.length
!== 0) {
5798 this.caret
.end($prev
);
5801 this.core
.editor().focus();
5807 this.keydown
.onBackspaceAndDeleteBefore();
5810 if (key
=== this.keyCode
.DELETE
) {
5811 var $next
= $(this.keydown
.block
).next();
5814 if (this.utils
.isEndOfElement(this.keydown
.block
) && $next
.length
!== 0 && $next
[0].tagName
=== 'FIGURE') {
5819 // append list (safari bug)
5820 var tagLi
= (this.keydown
.block
&& this.keydown
.block
.tagName
=== 'LI') ? this.keydown
.block
: false;
5822 var $list
= $(this.keydown
.block
).parents('ul, ol').last();
5823 var $nextList
= $list
.next();
5825 if (this.utils
.isRedactorParent($list
) && this.utils
.isEndOfElement(
5826 $list
) && $nextList
.length
!== 0 && ($nextList
[0].tagName
=== 'UL' || $nextList
[0].tagName
=== 'OL')) {
5829 $list
.append($nextList
.contents());
5837 if (this.utils
.isEndOfElement(this.keydown
.block
) && $next
.length
!== 0 && $next
[0].tagName
=== 'PRE') {
5838 $(this.keydown
.block
).append($next
.text());
5846 if (key
=== this.keyCode
.DELETE
&& $('#redactor-image-box').length
!== 0) {
5847 this.image
.remove();
5851 if (key
=== this.keyCode
.BACKSPACE
) {
5852 if (this.detect
.isFirefox()) {
5853 this.line
.removeOnBackspace(e
);
5856 // combine list after and before if paragraph is empty
5857 if (this.list
.combineAfterAndBefore(this.keydown
.block
)) {
5862 // backspace as outdent
5863 var block
= this.selection
.block();
5864 if (block
&& block
.tagName
=== 'LI' && this.utils
.isCollapsed() && this.utils
.isStartOfElement()) {
5865 this.indent
.decrease();
5870 this.keydown
.removeInvisibleSpace();
5871 this.keydown
.removeEmptyListInTable(e
);
5875 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
5876 this.keydown
.onBackspaceAndDeleteAfter(e
);
5880 onShiftSpace: function () {
5882 this.insert
.raw(' ');
5886 onShiftEnter: function (e
) {
5889 return (this.keydown
.pre
) ? this.keydown
.insertNewLine(e
) : this.insert
.raw(
5892 onBackspaceAndDeleteBefore: function () {
5893 this.utils
.saveScroll();
5895 onBackspaceAndDeleteAfter: function (e
) {
5897 setTimeout($.proxy(function () {
5898 this.code
.syncFire
= false;
5899 this.keydown
.removeEmptyLists();
5902 if (this.opts
.keepStyleAttr
.length
!== 0) {
5903 filter
= ',' + this.opts
.keepStyleAttr
.join(',');
5906 var $styleTags
= this.core
.editor().find('*[style]');
5908 'img, figure, iframe, #redactor-image-box, #redactor-image-editter, [data-redactor-style-cache], [data-redactor-span]' + filter
).removeAttr(
5911 this.keydown
.formatEmpty(e
);
5912 this.code
.syncFire
= true;
5916 onEnter: function (e
) {
5917 var stop
= this.core
.callback('enter', e
);
5918 if (stop
=== false) {
5924 if (this.keydown
.blockquote
&& this.keydown
.exitFromBlockquote(e
) === true) {
5929 if (this.keydown
.pre
) {
5930 return this.keydown
.insertNewLine(e
);
5932 // blockquote & figcaption
5933 else if (this.keydown
.blockquote
|| this.keydown
.figcaption
) {
5934 return this.keydown
.insertBreakLine(e
);
5937 else if (this.keydown
.figure
) {
5938 setTimeout($.proxy(function () {
5939 this.keydown
.replaceToParagraph('FIGURE');
5944 else if (this.keydown
.block
) {
5945 setTimeout($.proxy(function () {
5946 this.keydown
.replaceToParagraph('DIV');
5951 if (this.keydown
.block
.tagName
=== 'LI') {
5952 var current
= this.selection
.current();
5953 var $parent
= $(current
).closest('li', this.$editor
[0]);
5954 var $list
= $parent
.parents('ul,ol', this.$editor
[0]).last();
5956 if ($parent
.length
!== 0 && this.utils
.isEmpty($parent
.html()) && $list
.next().length
=== 0 && this.utils
.isEmpty(
5957 $list
.find('li').last().html())) {
5958 $list
.find('li').last().remove();
5960 var node
= $(this.opts
.emptyHtml
);
5962 this.caret
.start(node
);
5970 else if (!this.keydown
.block
) {
5971 return this.keydown
.insertParagraph(e
);
5974 // firefox enter into inline element
5975 if (this.detect
.isFirefox() && this.utils
.isInline(this.keydown
.parent
)) {
5976 this.keydown
.insertBreakLine(e
);
5980 // remove inline tags in new-empty paragraph
5981 if (!this.opts
.keepInlineOnEnter
) {
5982 setTimeout($.proxy(function () {
5983 var inline
= this.selection
.inline();
5984 if (inline
&& this.utils
.isEmpty(inline
.innerHTML
)) {
5985 var parent
= this.selection
.block();
5987 //this.caret.start(parent);
5989 var range
= document
.createRange();
5990 range
.setStart(parent
, 0);
5992 var textNode
= document
.createTextNode('\u200B');
5994 range
.insertNode(textNode
);
5995 range
.setStartAfter(textNode
);
5996 range
.collapse(true);
5998 var sel
= window
.getSelection();
5999 sel
.removeAllRanges();
6000 sel
.addRange(range
);
6006 checkEvents: function (arrow
, key
) {
6007 if (!arrow
&& (this.core
.getEvent() === 'click' || this.core
.getEvent() === 'arrow')) {
6008 this.core
.addEvent(false);
6010 if (this.keydown
.checkKeyEvents(key
)) {
6015 checkKeyEvents: function (key
) {
6016 var k
= this.keyCode
;
6029 return ($.inArray(key
, keys
) === -1) ? true : false;
6032 addArrowsEvent: function (arrow
) {
6037 if ((this.core
.getEvent() === 'click' || this.core
.getEvent() === 'arrow')) {
6038 this.core
.addEvent(false);
6042 this.core
.addEvent('arrow');
6044 setupBuffer: function (e
, key
) {
6045 if (this.keydown
.ctrl
&& key
=== 90 && !e
.shiftKey
&& !e
.altKey
&& this.sBuffer
.length
) // z key
6052 else if (this.keydown
.ctrl
&& key
=== 90 && e
.shiftKey
&& !e
.altKey
&& this.sRebuffer
.length
!== 0) {
6057 else if (!this.keydown
.ctrl
) {
6058 if (key
=== this.keyCode
.SPACE
|| key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
|| (key
=== this.keyCode
.ENTER
&& !e
.ctrlKey
&& !e
.shiftKey
)) {
6063 exitFromBlockquote: function (e
) {
6064 if (!this.utils
.isEndOfElement(this.keydown
.blockquote
)) {
6068 var tmp
= this.clean
.removeSpacesHard($(this.keydown
.blockquote
).html());
6069 if (tmp
.search(/(<br\s?\/?>){1}$/i) !== -1) {
6072 var $last
= $(this.keydown
.blockquote
).children().last();
6074 $last
.filter('br').remove();
6075 $(this.keydown
.blockquote
).children().last().filter('span').remove();
6077 var node
= $(this.opts
.emptyHtml
);
6078 $(this.keydown
.blockquote
).after(node
);
6079 this.caret
.start(node
);
6087 onArrowDown: function () {
6088 var tags
= [this.keydown
.blockquote
, this.keydown
.pre
, this.keydown
.figcaption
];
6090 for (var i
= 0; i
< tags
.length
; i
++) {
6092 this.keydown
.insertAfterLastElement(tags
[i
]);
6097 onArrowUp: function () {
6098 var tags
= [this.keydown
.blockquote
, this.keydown
.pre
, this.keydown
.figcaption
];
6100 for (var i
= 0; i
< tags
.length
; i
++) {
6102 this.keydown
.insertBeforeFirstElement(tags
[i
]);
6107 insertAfterLastElement: function (element
) {
6108 if (!this.utils
.isEndOfElement(element
)) {
6112 var last
= this.core
.editor().contents().last();
6113 var $next
= (element
.tagName
=== 'FIGCAPTION') ? $(this.keydown
.block
).parent().next() : $(
6114 this.keydown
.block
).next();
6116 if ($next
.length
!== 0) {
6119 else if (last
.length
=== 0 && last
[0] !== element
) {
6120 this.caret
.start(last
);
6124 var node
= $(this.opts
.emptyHtml
);
6126 if (element
.tagName
=== 'FIGCAPTION') {
6127 $(element
).parent().after(node
);
6130 $(element
).after(node
);
6133 this.caret
.start(node
);
6137 insertBeforeFirstElement: function (element
) {
6138 if (!this.utils
.isStartOfElement()) {
6142 if (this.core
.editor().contents().length
> 1 && this.core
.editor().contents().first()[0] !== element
) {
6146 var node
= $(this.opts
.emptyHtml
);
6147 $(element
).before(node
);
6148 this.caret
.start(node
);
6151 onTab: function (e
, key
) {
6152 if (!this.opts
.tabKey
) {
6156 var isList
= (this.keydown
.block
&& this.keydown
.block
.tagName
=== 'LI');
6157 if (this.utils
.isEmpty(this.code
.get()) || (!isList
&& !this.keydown
.pre
&& this.opts
.tabAsSpaces
=== false)) {
6164 var isListStart
= (isList
&& this.utils
.isStartOfElement(this.keydown
.block
));
6167 if (this.keydown
.pre
&& !e
.shiftKey
) {
6168 node
= (this.opts
.preSpaces
) ? document
.createTextNode(Array(this.opts
.preSpaces
+ 1).join(
6169 '\u00a0')) : document
.createTextNode('\t');
6170 this.insert
.node(node
);
6172 else if (this.opts
.tabAsSpaces
!== false && !isListStart
) {
6173 node
= document
.createTextNode(Array(this.opts
.tabAsSpaces
+ 1).join(
6175 this.insert
.node(node
);
6178 if (e
.metaKey
&& key
=== 219) {
6179 this.indent
.decrease();
6181 else if (e
.metaKey
&& key
=== 221) {
6182 this.indent
.increase();
6184 else if (!e
.shiftKey
) {
6185 this.indent
.increase();
6188 this.indent
.decrease();
6194 setupSelectAll: function (e
, key
) {
6195 if (this.keydown
.ctrl
&& key
=== 65) {
6196 this.utils
.enableSelectAll();
6198 else if (key
!== this.keyCode
.LEFT_WIN
&& !this.keydown
.ctrl
) {
6199 this.utils
.disableSelectAll();
6202 insertNewLine: function (e
) {
6205 var node
= document
.createTextNode('\n');
6207 var sel
= this.selection
.get();
6208 var range
= this.selection
.range(sel
);
6210 range
.deleteContents();
6211 range
.insertNode(node
);
6213 this.caret
.after(node
);
6217 insertParagraph: function (e
) {
6220 var p
= document
.createElement('p');
6221 //p.innerHTML = this.opts.invisibleSpace;
6222 p
.innerHTML
= '<br>';
6224 var sel
= this.selection
.get();
6225 var range
= this.selection
.range(sel
);
6227 range
.deleteContents();
6228 range
.insertNode(p
);
6230 this.caret
.start(p
);
6234 insertBreakLine: function (e
) {
6235 return this.keydown
.insertBreakLineProcessing(e
);
6237 insertDblBreakLine: function (e
) {
6238 return this.keydown
.insertBreakLineProcessing(e
, true);
6240 insertBreakLineProcessing: function (e
, dbl
) {
6241 e
.stopPropagation();
6243 var br1
= document
.createElement('br');
6244 this.insert
.node(br1
);
6247 var br2
= document
.createElement('br');
6248 this.insert
.node(br2
);
6249 this.caret
.after(br2
);
6252 this.caret
.after(br1
);
6258 wrapToParagraph: function () {
6259 var $current
= $(this.keydown
.current
);
6260 var node
= $('<p>').append($current
.clone());
6261 $current
.replaceWith(node
);
6263 var next
= $(node
).next();
6264 if (typeof (next
[0]) !== 'undefined' && next
[0].tagName
=== 'BR') {
6268 this.caret
.end(node
);
6271 replaceToParagraph: function (tag
) {
6272 var blockElem
= this.selection
.block();
6273 var $prev
= $(blockElem
).prev();
6275 var blockHtml
= blockElem
.innerHTML
.replace(/<br\s?\/?>/gi, '');
6276 if (blockElem
.tagName
=== tag
&& this.utils
.isEmpty(blockHtml
) && !$(blockElem
).hasClass(
6278 var p
= document
.createElement('p');
6279 $(blockElem
).replaceWith(p
);
6281 this.keydown
.setCaretToParagraph(p
);
6285 else if (blockElem
.tagName
=== 'P') {
6286 $(blockElem
).removeAttr('class').removeAttr('style');
6289 if (this.detect
.isIe() && this.utils
.isEmpty(blockHtml
) && this.utils
.isInline(
6290 this.keydown
.parent
)) {
6291 $(blockElem
).on('input', $.proxy(function () {
6292 var parent
= this.selection
.parent();
6293 if (this.utils
.isInline(parent
)) {
6294 var html
= $(parent
).html();
6295 $(blockElem
).html(html
);
6296 this.caret
.end(blockElem
);
6299 $(blockElem
).off('keyup');
6306 else if ($prev
.hasClass(this.opts
.videoContainerClass
)) {
6307 $prev
.removeAttr('class');
6309 var p
= document
.createElement('p');
6310 $prev
.replaceWith(p
);
6312 this.keydown
.setCaretToParagraph(p
);
6317 setCaretToParagraph: function (p
) {
6318 var range
= document
.createRange();
6319 range
.setStart(p
, 0);
6321 var textNode
= document
.createTextNode('\u200B');
6323 range
.insertNode(textNode
);
6324 range
.setStartAfter(textNode
);
6325 range
.collapse(true);
6327 var sel
= window
.getSelection();
6328 sel
.removeAllRanges();
6329 sel
.addRange(range
);
6331 removeInvisibleSpace: function () {
6332 var $current
= $(this.keydown
.current
);
6333 if ($current
.text().search(/^\u200B$/g) === 0) {
6337 removeEmptyListInTable: function (e
) {
6338 var $current
= $(this.keydown
.current
);
6339 var $parent
= $(this.keydown
.parent
);
6340 var td
= $current
.closest('td', this.$editor
[0]);
6342 if (td
.length
!== 0 && $current
.closest('li',
6344 ) && $parent
.children('li').length
=== 1) {
6345 if (!this.utils
.isEmpty($current
.text())) {
6354 this.caret
.start(td
);
6357 removeEmptyLists: function () {
6358 var removeIt = function () {
6359 var html
= $.trim(this.innerHTML
).replace(/\/t\/n/g, '');
6365 this.core
.editor().find('li').each(removeIt
);
6366 this.core
.editor().find('ul, ol').each(removeIt
);
6368 formatEmpty: function (e
) {
6369 var html
= $.trim(this.core
.editor().html());
6371 if (!this.utils
.isEmpty(html
)) {
6377 if (this.opts
.type
=== 'inline' || this.opts
.type
=== 'pre') {
6378 this.core
.editor().html(this.marker
.html());
6379 this.selection
.restore();
6382 var updateHtml = function() {
6383 this.core
.editor().html(this.opts
.emptyHtml
);
6387 if (Environment
!== null && Environment
.platform() === 'ios') {
6388 // In iOS Safari the backspace sometimes appears to be triggered twice if the editor
6389 // is completely empty. After debugging for way too much time, and realizing that
6390 // the remote debugger's breakpoints alter the behavior of async callbacks (*), this
6391 // should solve the issue.
6393 // (*) Set up a `console.log()` inside a MutationObserver and then make use of the
6394 // `debugger;` statement to halt the execution flow. The observer is executed, but
6395 // the output never appears on the console. Output works if there is no breakpoint.
6396 setTimeout(updateHtml
, 50);
6410 keyup: function () {
6412 init: function (e
) {
6413 if (this.rtePaste
) {
6418 this.keyup
.block
= this.selection
.block();
6419 this.keyup
.current
= this.selection
.current();
6420 this.keyup
.parent
= this.selection
.parent();
6421 this.keyup
.lastShiftKey
= e
.shiftKey
;
6424 var stop
= this.core
.callback('keyup', e
);
6425 if (stop
=== false) {
6430 // replace a prev figure to paragraph if caret is before image
6431 if (key
=== this.keyCode
.ENTER
) {
6432 if (this.keyup
.block
&& this.keyup
.block
.tagName
=== 'FIGURE') {
6433 var $prev
= $(this.keyup
.block
).prev();
6434 if ($prev
.length
!== 0 && $prev
[0].tagName
=== 'FIGURE') {
6435 var $newTag
= this.utils
.replaceToTag($prev
, 'p');
6436 this.caret
.start($newTag
);
6442 // replace figure to paragraph
6443 if (key
=== this.keyCode
.BACKSPACE
|| key
=== this.keyCode
.DELETE
) {
6444 if (this.utils
.isSelectAll()) {
6450 // if caret before figure - delete image
6451 if (this.keyup
.block
&& this.keydown
.block
&& this.keyup
.block
.tagName
=== 'FIGURE' && this.utils
.isStartOfElement(
6452 this.keydown
.block
)) {
6455 this.selection
.save();
6456 $(this.keyup
.block
).find('figcaption').remove();
6457 $(this.keyup
.block
).find('img').first().remove();
6458 this.utils
.replaceToTag(this.keyup
.block
, 'p');
6460 var $marker
= this.marker
.find();
6461 $('html, body').animate({scrollTop
: $marker
.position().top
+ 20},
6465 this.selection
.restore();
6469 // if paragraph does contain only image replace to figure
6470 if (this.keyup
.block
&& this.keyup
.block
.tagName
=== 'P') {
6471 var isContainImage
= $(this.keyup
.block
).find('img').length
;
6472 var text
= $(this.keyup
.block
).text().replace(/\u200B/g, '');
6473 if (text
=== '' && isContainImage
!== 0) {
6474 this.utils
.replaceToTag(this.keyup
.block
, 'figure');
6478 // if figure does not contain image - replace to paragraph
6479 if (this.keyup
.block
&& this.keyup
.block
.tagName
=== 'FIGURE' && $(this.keyup
.block
).find(
6480 'img').length
=== 0) {
6481 this.selection
.save();
6482 this.utils
.replaceToTag(this.keyup
.block
, 'p');
6483 this.selection
.restore();
6495 this.opts
.curLang
= this.opts
.langs
[this.opts
.lang
];
6497 get: function (name
) {
6498 return (typeof this.opts
.curLang
[name
] !== 'undefined') ? this.opts
.curLang
[name
] : '';
6506 insert: function () {
6510 this.insert
.html(this.line
.getLineHtml());
6513 var $hr
= this.core
.editor().find('#redactor-hr-tmp-id');
6514 $hr
.removeAttr('id');
6516 this.core
.callback('insertedLine', $hr
);
6520 getLineHtml: function () {
6521 var html
= '<hr id="redactor-hr-tmp-id" />';
6522 if (!this.detect
.isFirefox() && this.utils
.isEmpty()) {
6523 html
+= '<p>' + this.opts
.emptyHtml
+ '</p>';
6528 removeOnBackspace: function (e
) {
6529 if (!this.utils
.isCollapsed()) {
6533 var $block
= $(this.selection
.block());
6534 if ($block
.length
=== 0 || !this.utils
.isStartOfElement($block
)) {
6538 // if hr is previous element
6539 var $prev
= $block
.prev();
6540 if ($prev
&& $prev
.length
!== 0 && $prev
[0].tagName
=== 'HR') {
6554 return $(this.selection
.inlines('a'));
6557 var nodes
= this.selection
.nodes();
6558 var $link
= $(this.selection
.current()).closest('a', this.core
.editor()[0]);
6560 return ($link
.length
=== 0 || nodes
.length
> 1) ? false : $link
;
6562 unlink: function (e
) {
6563 // if call from clickable element
6564 if (typeof e
!== 'undefined' && e
.preventDefault
) {
6571 var links
= this.selection
.inlines('a');
6572 if (links
.length
=== 0) {
6576 var $links
= this.link
.replaceLinksToText(links
);
6578 this.observe
.closeAllTooltip();
6579 this.core
.callback('deletedLink', $links
);
6582 insert: function (link
, cleaned
) {
6583 var $el
= this.link
.is();
6585 if (cleaned
!== true) {
6586 link
= this.link
.buildLinkFromObject($el
, link
);
6587 if (link
=== false) {
6596 link
= this.core
.callback('beforeInsertingLink', link
);
6598 if ($el
=== false) {
6601 $el
= this.link
.update($el
, link
);
6602 $el
= $(this.insert
.node($el
));
6604 var $parent
= $el
.parent();
6605 if (this.utils
.isRedactorParent($parent
) === false) {
6609 // remove unlink wrapper
6610 if ($parent
.hasClass('redactor-unlink')) {
6611 $parent
.replaceWith(function () {
6612 return $(this).contents();
6616 this.caret
.after($el
);
6617 this.core
.callback('insertedLink', $el
);
6621 $el
= this.link
.update($el
, link
);
6622 this.caret
.after($el
);
6628 update: function ($el
, link
) {
6629 $el
.text(link
.text
);
6630 $el
.attr('href', link
.url
);
6632 this.link
.target($el
, link
.target
);
6637 target: function ($el
, target
) {
6638 return (target
) ? $el
.attr('target', '_blank') : $el
.removeAttr('target');
6640 show: function (e
) {
6641 // if call from clickable element
6642 if (typeof e
!== 'undefined' && e
.preventDefault
) {
6647 this.observe
.closeAllTooltip();
6650 var $el
= this.link
.is();
6653 this.link
.buildModal($el
);
6656 var link
= this.link
.buildLinkFromElement($el
);
6658 // if link cut & paste inside editor browser added self host to a link
6659 link
.url
= this.link
.removeSelfHostFromUrl(link
.url
);
6662 if (this.opts
.linkNewTab
&& !$el
) {
6667 this.link
.setModalValues(link
);
6673 if (this.detect
.isDesktop()) {
6674 $('#redactor-link-url').focus();
6679 setModalValues: function (link
) {
6680 $('#redactor-link-blank').prop('checked', link
.target
);
6681 $('#redactor-link-url').val(link
.url
);
6682 $('#redactor-link-url-text').val(link
.text
);
6684 buildModal: function ($el
) {
6685 this.modal
.load('link',
6686 this.lang
.get(($el
=== false) ? 'link-insert' : 'link-edit'),
6691 var $btn
= this.modal
.getActionButton();
6692 $btn
.text(this.lang
.get(($el
=== false) ? 'insert' : 'save')).on('click',
6693 $.proxy(this.link
.callback
, this)
6697 callback: function () {
6699 var link
= this.link
.buildLinkFromModal();
6700 if (link
=== false) {
6708 this.link
.insert(link
, true);
6710 cleanUrl: function (url
) {
6711 return (typeof url
=== 'undefined') ? '' : $.trim(url
.replace(/[^\W\w\D\d+&\'@#/%?=~_
|!:,.;\(\)]/gi
,
6715 cleanText: function (text
) {
6716 return (typeof text
=== 'undefined') ? '' : $.trim(text
.replace(/(<([^>]+)>)/gi,
6720 getText: function (link
) {
6721 return (link
.text
=== '' && link
.url
!== '') ? this.link
.truncateUrl(link
.url
.replace(/<|>/g,
6725 isUrl: function (url
) {
6726 var reUrl
= new RegExp(
6727 '^((https?|ftp):\\/\\/)?(([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*(\\?[;&a-z\\d%_.~+=-]*)?(\\#[-a-z\\d_]*)?$',
6731 return (reUrl
.test(url
)) ? url
: false;
6733 isMailto: function (url
) {
6734 return (url
.search('@') !== -1 && /(http|ftp|https):\/\//i.test(url
) === false);
6736 isEmpty: function (link
) {
6737 return (link
.url
=== '' || (link
.text
=== '' && link
.url
=== ''));
6739 truncateUrl: function (url
) {
6740 return (url
.length
> this.opts
.linkSize
) ? url
.substring(0,
6744 parse: function (link
) {
6746 if (this.link
.isMailto(link
.url
)) {
6747 link
.url
= 'mailto:' + link
.url
.replace('mailto:', '');
6750 else if (link
.url
.search('#') !== 0) {
6751 if (this.opts
.linkValidation
) {
6752 link
.url
= (this.link
.isUrl(link
.url
)) ? 'http://' + link
.url
.replace(/(ftp
|https
?):\/\//gi,
6758 // empty url or text or isn't url
6759 return (this.link
.isEmpty(link
) || link
.url
=== false) ? false : link
;
6762 buildLinkFromModal: function () {
6766 link
.url
= this.link
.cleanUrl($('#redactor-link-url').val());
6769 link
.text
= this.link
.cleanText($('#redactor-link-url-text').val());
6770 link
.text
= this.link
.getText(link
);
6773 link
.target
= ($('#redactor-link-blank').prop('checked')) ? true : false;
6776 return this.link
.parse(link
);
6779 buildLinkFromObject: function ($el
, link
) {
6781 link
.url
= this.link
.cleanUrl(link
.url
);
6784 link
.text
= (typeof link
.text
=== 'undefined' && this.selection
.is()) ? this.selection
.text() : this.link
.cleanText(
6786 link
.text
= this.link
.getText(link
);
6789 link
.target
= ($el
=== false) ? link
.target
: this.link
.buildTarget($el
);
6792 return this.link
.parse(link
);
6795 buildLinkFromElement: function ($el
) {
6798 text
: (this.selection
.is()) ? this.selection
.text() : '',
6802 if ($el
!== false) {
6803 link
.url
= $el
.attr('href');
6804 link
.text
= $el
.text();
6805 link
.target
= this.link
.buildTarget($el
);
6810 buildTarget: function ($el
) {
6811 return (typeof $el
.attr('target') !== 'undefined' && $el
.attr('target') === '_blank') ? true : false;
6813 removeSelfHostFromUrl: function (url
) {
6814 var href
= self
.location
.href
.replace('#', '').replace(/\/$/i, '');
6815 return url
.replace(/^\/\#/, '#').replace(href
, '').replace('mailto:', '');
6817 replaceLinksToText: function (links
) {
6819 var $links
= $.each(links
, function (i
, s
) {
6821 var $unlinked
= $('<span class="redactor-unlink" />').append($el
.contents());
6822 $el
.replaceWith($unlinked
);
6831 // set caret after unlinked node
6832 if (links
.length
=== 1 && this.selection
.isCollapsed()) {
6833 this.caret
.after($first
);
6841 // =linkify -- UNSUPPORTED MODULE
6842 linkify: function () {
6844 isKey: function () {},
6845 isLink: function () {},
6846 isFiltered: function () {},
6847 handler: function () {},
6848 format: function () {},
6849 convertVideoLinks: function () {},
6850 convertImages: function () {},
6851 convertLinks: function () {}
6858 toggle: function (type
) {
6859 if (this.utils
.inBlocks(['table', 'td', 'th', 'tr'])) {
6863 type
= (type
=== 'orderedlist') ? 'ol' : type
;
6864 type
= (type
=== 'unorderedlist') ? 'ul' : type
;
6866 type
= type
.toLowerCase();
6869 this.selection
.save();
6871 var nodes
= this.list
._getBlocks();
6872 var block
= this.selection
.block();
6873 var $list
= $(block
).parents('ul, ol').last();
6874 if (nodes
.length
=== 0 && $list
.length
!== 0) {
6875 nodes
= [$list
.get(0)];
6878 nodes
= (this.list
._isUnformat(type
, nodes
)) ? this.list
._unformat(type
,
6880 ) : this.list
._format(type
, nodes
);
6882 this.selection
.restore();
6887 var current
= this.selection
.current();
6888 var $list
= $(current
).closest('ul, ol', this.core
.editor()[0]);
6890 return ($list
.length
=== 0) ? false : $list
;
6892 combineAfterAndBefore: function (block
) {
6893 var $prev
= $(block
).prev();
6894 var $next
= $(block
).next();
6895 var isEmptyBlock
= (block
&& block
.tagName
=== 'P' && (block
.innerHTML
=== '<br>' || block
.innerHTML
=== ''));
6896 var isBlockWrapped
= ($prev
.closest('ol, ul',
6897 this.core
.editor()[0]
6898 ).length
=== 1 && $next
.closest(
6900 this.core
.editor()[0]
6903 if (isEmptyBlock
&& isBlockWrapped
) {
6904 $prev
.children('li').last().append(this.marker
.get());
6905 $prev
.append($next
.contents());
6906 this.selection
.restore();
6914 _getBlocks: function () {
6915 var finalBlocks
= [];
6916 var blocks
= this.selection
.blocks();
6917 for (var i
= 0; i
< blocks
.length
; i
++) {
6918 var $el
= $(blocks
[i
]);
6919 var isFirst
= ($el
.parent().hasClass('redactor-in'));
6921 if (isFirst
) finalBlocks
.push(blocks
[i
]);
6926 _isUnformat: function (type
, nodes
) {
6928 for (var i
= 0; i
< nodes
.length
; i
++) {
6929 if (nodes
[i
].nodeType
!== 3) {
6930 var tag
= nodes
[i
].tagName
.toLowerCase();
6931 if (tag
=== type
|| tag
=== 'figure') {
6937 return (countLists
=== nodes
.length
);
6939 _uniteBlocks: function (nodes
, tags
) {
6941 var blocks
= {0: []};
6942 var lastcell
= false;
6943 for (var i
= 0; i
< nodes
.length
; i
++) {
6944 var $node
= $(nodes
[i
]);
6945 var $cell
= $node
.closest('th, td');
6947 if ($cell
.length
!== 0) {
6948 if ($cell
.get(0) !== lastcell
) {
6954 if (this.list
._isUniteBlock(nodes
[i
], tags
)) {
6955 blocks
[z
].push(nodes
[i
]);
6959 if (this.list
._isUniteBlock(nodes
[i
], tags
)) {
6960 blocks
[z
].push(nodes
[i
]);
6969 lastcell
= $cell
.get();
6974 _isUniteBlock: function (node
, tags
) {
6975 return (node
.nodeType
=== 3 || tags
.indexOf(node
.tagName
.toLowerCase()) !== -1);
6977 _createList: function (type
, blocks
, key
) {
6978 var last
= blocks
[blocks
.length
- 1];
6979 var $last
= $(last
);
6980 var $list
= $('<' + type
+ '>');
6985 _createListItem: function (item
) {
6986 var $item
= $('<li>');
6987 if (item
.nodeType
=== 3) {
6992 $item
.append($el
.contents());
6998 _format: function (type
, nodes
) {
7013 var blocks
= this.list
._uniteBlocks(nodes
, tags
);
7016 for (var key
in blocks
) {
7017 var items
= blocks
[key
];
7018 var $list
= this.list
._createList(type
, blocks
[key
]);
7020 for (var i
= 0; i
< items
.length
; i
++) {
7024 if (items
[i
].nodeType
!== 3 && (items
[i
].tagName
=== 'UL' || items
[i
].tagName
=== 'OL')) {
7025 $item
= $(items
[i
]).contents();
7026 $list
.append($item
);
7028 // other blocks or texts
7030 $item
= this.list
._createListItem(items
[i
]);
7031 //this.utils.normalizeTextNodes($item);
7032 $list
.append($item
);
7036 lists
.push($list
.get(0));
7041 _unformat: function (type
, nodes
) {
7043 if (nodes
.length
=== 1) {
7045 var $list
= $(nodes
[0]);
7046 var $items
= $list
.find('li');
7048 var selectedItems
= this.selection
.blocks(['li']);
7049 var block
= this.selection
.block();
7050 var $li
= $(block
).closest('li');
7051 if (selectedItems
.length
=== 0 && $li
.length
!== 0) {
7052 selectedItems
= [$li
.get(0)];
7056 if (selectedItems
.length
=== $items
.length
) {
7057 return this.list
._unformatEntire(nodes
[0]);
7060 var pos
= this.list
._getItemsPosition($items
, selectedItems
);
7063 if (pos
=== 'Top') {
7064 return this.list
._unformatAtSide('before',
7071 else if (pos
=== 'Bottom') {
7072 selectedItems
.reverse();
7073 return this.list
._unformatAtSide('after', selectedItems
, $list
);
7077 else if (pos
=== 'Middle') {
7078 var $last
= $(selectedItems
[selectedItems
.length
- 1]);
7082 var $parent
= false;
7083 var $secondList
= $('<' + $list
.get(0).tagName
.toLowerCase() + '>');
7084 $items
.each(function (i
, node
) {
7086 var $node
= $(node
);
7087 var $childList
= ($node
.children('ul, ol').length
!== 0);
7089 if ($node
.closest('.redactor-split-item').length
=== 0 && ($parent
=== false || $node
.closest(
7090 $parent
).length
=== 0)) {
7091 $node
.addClass('redactor-split-item');
7098 if (node
=== $last
.get(0)) {
7103 $items
.filter('.redactor-split-item').each(function (i
, node
) {
7104 var $node
= $(node
);
7105 $node
.removeClass('redactor-split-item');
7106 $secondList
.append(node
);
7109 $list
.after($secondList
);
7111 selectedItems
.reverse();
7112 for (var i
= 0; i
< selectedItems
.length
; i
++) {
7113 var $item
= $(selectedItems
[i
]);
7114 var $container
= this.list
._createUnformatContainer(
7117 $list
.after($container
);
7118 $container
.find('ul, ol').remove();
7128 for (var i
= 0; i
< nodes
.length
; i
++) {
7129 if (nodes
[i
].nodeType
!== 3 && nodes
[i
].tagName
.toLowerCase() === type
) {
7130 this.list
._unformatEntire(nodes
[i
]);
7135 _unformatEntire: function (list
) {
7136 var $list
= $(list
);
7137 var $items
= $list
.find('li');
7138 $items
.each(function (i
, node
) {
7139 var $item
= $(node
);
7140 var $container
= this.list
._createUnformatContainer($item
);
7143 $list
.before($container
);
7149 _unformatAtSide: function (type
, selectedItems
, $list
) {
7150 for (var i
= 0; i
< selectedItems
.length
; i
++) {
7151 var $item
= $(selectedItems
[i
]);
7152 var $container
= this.list
._createUnformatContainer($item
);
7154 $list
[type
]($container
);
7156 var $innerLists
= $container
.find('ul, ol').first();
7157 $item
.append($innerLists
);
7159 $innerLists
.each(function (i
, node
) {
7160 var $node
= $(node
);
7161 var $parent
= $node
.closest('li');
7163 if ($parent
.get(0) === selectedItems
[i
]) {
7165 $parent
.addClass('r-unwrapped');
7170 if (this.utils
.isEmpty($item
.html())) $item
.remove();
7174 $list
.find('.r-unwrapped').each(function (node
) {
7175 var $node
= $(node
);
7176 if ($node
.html().trim() === '') {
7180 $node
.removeClass('r-unwrapped');
7184 _getItemsPosition: function ($items
, selectedItems
) {
7187 var sFirst
= selectedItems
[0];
7188 var sLast
= selectedItems
[selectedItems
.length
- 1];
7190 var first
= $items
.first().get(0);
7191 var last
= $items
.last().get(0);
7193 if (first
=== sFirst
&& last
!== sLast
) {
7196 else if (first
!== sFirst
&& last
=== sLast
) {
7202 _createUnformatContainer: function ($item
) {
7203 var $container
= $('<p>');
7204 $container
.append($item
.contents());
7212 marker: function () {
7216 get: function (num
) {
7217 num
= (typeof num
=== 'undefined') ? 1 : num
;
7219 var marker
= document
.createElement('span');
7221 marker
.id
= 'selection-marker-' + num
;
7222 marker
.className
= 'redactor-selection-marker';
7223 marker
.innerHTML
= this.opts
.invisibleSpace
;
7227 html: function (num
) {
7228 return this.utils
.getOuterHtml(this.marker
.get(num
));
7230 find: function (num
) {
7231 num
= (typeof num
=== 'undefined') ? 1 : num
;
7233 return this.core
.editor().find('span#selection-marker-' + num
);
7235 insert: function () {
7236 var sel
= this.selection
.get();
7237 var range
= this.selection
.range(sel
);
7239 this.marker
.insertNode(range
, this.marker
.get(1), true);
7240 if (range
&& range
.collapsed
=== false) {
7241 this.marker
.insertNode(range
, this.marker
.get(2), false);
7245 remove: function () {
7246 this.core
.editor().find('.redactor-selection-marker').each(this.marker
.iterateRemove
);
7250 insertNode: function (range
, node
, collapse
) {
7251 var parent
= this.selection
.parent();
7252 if (range
=== null || $(parent
).closest('.redactor-in').length
=== 0) {
7256 range
= range
.cloneRange();
7259 range
.collapse(collapse
);
7260 range
.insertNode(node
);
7266 iterateRemove: function (i
, el
) {
7268 var text
= $el
.text().replace(/\u200B/g, '');
7269 var parent
= $el
.parent()[0];
7271 if (text
=== '') $el
.remove(); else $el
.replaceWith(function () { return $(this).contents(); });
7273 // if (parent && parent.normalize) parent.normalize();
7279 modal: function () {
7282 templates: function () {
7290 'link': String() + '<div class="redactor-modal-tab" data-title="General">' + '<section>' + '<label>URL</label>' + '<input type="url" id="redactor-link-url" aria-label="URL" />' + '</section>' + '<section>' + '<label>' + this.lang
.get(
7291 'text') + '</label>' + '<input type="text" id="redactor-link-url-text" aria-label="' + this.lang
.get(
7292 'text') + '" />' + '</section>' + '<section>' + '<label class="checkbox"><input type="checkbox" id="redactor-link-blank"> ' + this.lang
.get(
7293 'link-in-new-tab') + '</label>' + '</section>' + '<section>' + '<button id="redactor-modal-button-action">' + this.lang
.get(
7294 'insert') + '</button>' + '<button id="redactor-modal-button-cancel">' + this.lang
.get(
7295 'cancel') + '</button>' + '</section>' + '</div>'
7298 $.extend(this.opts
, this.opts
.modal
);
7301 addCallback: function (name
, callback
) {
7302 this.modal
.callbacks
[name
] = callback
;
7304 addTemplate: function (name
, template
) {
7305 this.opts
.modal
[name
] = template
;
7307 getTemplate: function (name
) {
7308 return this.opts
.modal
[name
];
7310 getModal: function () {
7311 return this.$modalBody
;
7313 getActionButton: function () {
7314 return this.$modalBody
.find('#redactor-modal-button-action');
7316 getCancelButton: function () {
7317 return this.$modalBody
.find('#redactor-modal-button-cancel');
7319 getDeleteButton: function () {
7320 return this.$modalBody
.find('#redactor-modal-button-delete');
7322 load: function () { /* WoltLabModal.js */ },
7323 show: function () { /* WoltLabModal.js */ },
7324 buildWidth: function () { },
7325 buildTabber: function () {},
7326 showTab: function () {},
7327 setTitle: function () { /* WoltLabModal.js */ },
7328 setContent: function () {
7329 this.$modalBody
.html(this.modal
.getTemplate(this.modal
.templateName
));
7331 this.modal
.getCancelButton().on('mousedown', $.proxy(this.modal
.close
, this));
7333 setDraggable: function () {},
7334 setEnter: function () {},
7335 build: function () {
7336 this.modal
.buildOverlay();
7338 this.$modalBox
= $('<div id="redactor-modal-box"/>').hide();
7339 this.$modal
= $('<div id="redactor-modal" role="dialog" />');
7340 this.$modalHeader
= $('<div id="redactor-modal-header" />');
7341 this.$modalClose
= $(
7342 '<button type="button" id="redactor-modal-close" aria-label="' + this.lang
.get(
7343 'close') + '" />').html('×');
7344 this.$modalBody
= $('<div id="redactor-modal-body" />');
7346 this.$modal
.append(this.$modalHeader
);
7347 this.$modal
.append(this.$modalBody
);
7348 this.$modal
.append(this.$modalClose
);
7349 this.$modalBox
.append(this.$modal
);
7350 this.$modalBox
.appendTo(document
.body
);
7353 buildOverlay: function () {
7354 this.$modalOverlay
= $('<div id="redactor-modal-overlay">').hide();
7355 $('body').prepend(this.$modalOverlay
);
7357 enableEvents: function () {},
7358 disableEvents: function () {},
7359 closeHandler: function () {},
7360 close: function () { /* WoltLabModal.js */ }
7365 observe: function () {
7368 if (typeof this.opts
.destroyed
!== 'undefined') {
7372 this.observe
.links();
7373 this.observe
.images();
7376 isCurrent: function ($el
, $current
) {
7377 if (typeof $current
=== 'undefined') {
7378 $current
= $(this.selection
.current());
7381 return $current
.is($el
) || $current
.parents($el
).length
> 0;
7383 toolbar: function () {
7384 this.observe
.buttons();
7385 this.observe
.dropdowns();
7387 buttons: function (e
, btnName
) {
7388 var current
= this.selection
.current();
7389 var parent
= this.selection
.parent();
7392 this.button
.setInactiveAll();
7395 this.button
.setInactiveAll(btnName
);
7398 if (e
=== false && btnName
!== 'html') {
7399 if ($.inArray(btnName
, this.opts
.activeButtons
) !== -1) {
7400 this.button
.toggleActive(btnName
);
7405 if (!this.utils
.isRedactorParent(current
)) {
7410 if (this.core
.editor().css('display') !== 'none') {
7411 if (this.utils
.isCurrentOrParentHeader() || this.utils
.isCurrentOrParent(
7412 ['table', 'pre', 'blockquote', 'li'])) {
7413 this.button
.disable('horizontalrule');
7416 this.button
.enable('horizontalrule');
7420 $.each(this.opts
.activeButtonsStates
, $.proxy(function (key
, value
) {
7421 var parentEl
= $(parent
).closest(key
, this.$editor
[0]);
7422 var currentEl
= $(current
).closest(key
, this.$editor
[0]);
7424 if (parentEl
.length
!== 0 && !this.utils
.isRedactorParent(parentEl
)) {
7428 if (!this.utils
.isRedactorParent(currentEl
)) {
7432 if (parentEl
.length
!== 0 || currentEl
.closest(key
,
7435 this.button
.setActive(value
);
7441 dropdowns: function () {
7442 var finded
= $('<div />').html(this.selection
.html()).find('a').length
;
7443 var $current
= $(this.selection
.current());
7444 var isRedactor
= this.utils
.isRedactorParent($current
);
7446 $.each(this.opts
.observe
.dropdowns
, $.proxy(function (key
, value
) {
7447 var observe
= value
.observe
, element
= observe
.element
,
7449 inValues
= typeof observe
['in'] !== 'undefined' ? observe
['in'] : false,
7450 outValues
= typeof observe
.out
!== 'undefined' ? observe
.out
: false;
7452 if (($current
.closest(element
).length
> 0 && isRedactor
) || (element
=== 'a' && finded
!== 0)) {
7453 this.observe
.setDropdownProperties($item
, inValues
, outValues
);
7456 this.observe
.setDropdownProperties($item
, outValues
, inValues
);
7461 setDropdownProperties: function ($item
, addProperties
, deleteProperties
) {
7462 if (deleteProperties
&& typeof deleteProperties
.attr
!== 'undefined') {
7463 this.observe
.setDropdownAttr($item
, deleteProperties
.attr
, true);
7466 if (typeof addProperties
.attr
!== 'undefined') {
7467 this.observe
.setDropdownAttr($item
, addProperties
.attr
);
7470 if (typeof addProperties
.title
!== 'undefined') {
7471 $item
.find('span').text(addProperties
.title
);
7474 setDropdownAttr: function ($item
, properties
, isDelete
) {
7475 $.each(properties
, function (key
, value
) {
7476 if (key
=== 'class') {
7478 $item
.addClass(value
);
7481 $item
.removeClass(value
);
7486 $item
.attr(key
, value
);
7489 $item
.removeAttr(key
);
7494 addDropdown: function ($item
, btnName
, btnObject
) {
7495 if (typeof btnObject
.observe
=== 'undefined') {
7499 btnObject
.item
= $item
;
7501 this.opts
.observe
.dropdowns
.push(btnObject
);
7503 images: function () {
7504 if (this.opts
.imageEditable
) {
7505 this.core
.editor().addClass('redactor-layer-img-edit');
7506 this.core
.editor().find('img').each($.proxy(function (i
, img
) {
7509 // IE fix (when we clicked on an image and then press backspace IE does goes to image's url)
7510 $img
.closest('a', this.$editor
[0]).on('click',
7511 function (e
) { e
.preventDefault(); }
7514 this.image
.setEditable($img
);
7519 links: function () {
7520 if (this.opts
.linkTooltip
) {
7521 this.core
.editor().find('a').each($.proxy(function (i
, s
) {
7523 if ($link
.data('cached') !== true) {
7524 $link
.data('cached', true);
7526 'touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7527 $.proxy(this.observe
.showTooltip
, this)
7534 getTooltipPosition: function ($link
) {
7535 return $link
.offset();
7537 showTooltip: function (e
) {
7538 var $el
= $(e
.target
);
7540 if ($el
[0].tagName
=== 'IMG') {
7544 if ($el
[0].tagName
!== 'A') {
7545 $el
= $el
.closest('a', this.$editor
[0]);
7548 if ($el
[0].tagName
!== 'A') {
7554 var pos
= this.observe
.getTooltipPosition($link
);
7555 var tooltip
= $('<span class="redactor-link-tooltip"></span>');
7557 var href
= $link
.attr('href');
7558 if (href
=== undefined) {
7562 if (href
.length
> 24) {
7563 href
= href
.substring(0, 24) + '...';
7566 var aLink
= $('<a href="' + $link
.attr('href') + '" target="_blank" />').html(
7567 href
).addClass('redactor-link-tooltip-action');
7568 var aEdit
= $('<a href="#" />').html(this.lang
.get('edit')).on('click',
7569 $.proxy(this.link
.show
, this)
7570 ).addClass('redactor-link-tooltip-action');
7571 var aUnlink
= $('<a href="#" />').html(this.lang
.get('unlink')).on('click',
7572 $.proxy(this.link
.unlink
, this)
7573 ).addClass('redactor-link-tooltip-action');
7575 tooltip
.append(aLink
).append(' | ').append(aEdit
).append(' | ').append(aUnlink
);
7577 var lineHeight
= parseInt($link
.css('line-height'), 10);
7578 var lineClicked
= Math
.ceil((e
.pageY
- pos
.top
) / lineHeight
);
7579 var top
= pos
.top
+ lineClicked
* lineHeight
;
7583 left
: pos
.left
+ 'px'
7586 $('.redactor-link-tooltip').remove();
7587 $('body').append(tooltip
);
7589 this.core
.editor().on('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7590 $.proxy(this.observe
.closeTooltip
, this)
7592 $(document
).on('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7593 $.proxy(this.observe
.closeTooltip
, this)
7596 closeAllTooltip: function () {
7597 $('.redactor-link-tooltip').remove();
7599 closeTooltip: function (e
) {
7600 e
= e
.originalEvent
|| e
;
7602 var target
= e
.target
;
7603 var $parent
= $(target
).closest('a', this.$editor
[0]);
7604 if ($parent
.length
!== 0 && $parent
[0].tagName
=== 'A' && target
.tagName
!== 'A') {
7607 else if ((target
.tagName
=== 'A' && this.utils
.isRedactorParent(target
)) || $(
7608 target
).hasClass('redactor-link-tooltip-action')) {
7612 this.observe
.closeAllTooltip();
7614 this.core
.editor().off('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7615 $.proxy(this.observe
.closeTooltip
, this)
7617 $(document
).off('touchstart.redactor.' + this.uuid
+ ' click.redactor.' + this.uuid
,
7618 $.proxy(this.observe
.closeTooltip
, this)
7626 offset: function () {
7628 get: function (node
) {
7629 var cloned
= this.offset
.clone(node
);
7630 if (cloned
=== false) {
7634 var div
= document
.createElement('div');
7635 div
.appendChild(cloned
.cloneContents());
7636 div
.innerHTML
= div
.innerHTML
.replace(/<img(.*?[^>])>$/gi, 'i');
7638 var text
= $.trim($(div
).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
7645 clone: function (node
) {
7646 var sel
= this.selection
.get();
7647 var range
= this.selection
.range(sel
);
7649 if (range
=== null && typeof node
=== 'undefined') {
7653 node
= (typeof node
=== 'undefined') ? this.$editor
: node
;
7654 if (node
=== false) {
7658 node
= node
[0] || node
;
7660 var cloned
= range
.cloneRange();
7661 cloned
.selectNodeContents(node
);
7662 cloned
.setEnd(range
.endContainer
, range
.endOffset
);
7666 set: function (start
, end
) {
7667 end
= (typeof end
=== 'undefined') ? start
: end
;
7669 if (!this.focus
.is()) {
7673 var sel
= this.selection
.get();
7674 var range
= this.selection
.range(sel
);
7675 var node
, offset
= 0;
7676 var walker
= document
.createTreeWalker(this.$editor
[0],
7677 NodeFilter
.SHOW_TEXT
,
7682 while ((node
= walker
.nextNode()) !== null) {
7683 offset
+= node
.nodeValue
.length
;
7684 if (offset
> start
) {
7685 range
.setStart(node
, node
.nodeValue
.length
+ start
- offset
);
7689 if (offset
>= end
) {
7690 range
.setEnd(node
, node
.nodeValue
.length
+ end
- offset
);
7695 range
.collapse(false);
7696 this.selection
.update(sel
, range
);
7702 paragraphize: function () {
7704 load: function (html
) {
7705 if (this.opts
.paragraphize
=== false || this.opts
.type
=== 'inline' || this.opts
.type
=== 'pre') {
7709 if (html
=== '' || html
=== '<p></p>') {
7710 return this.opts
.emptyHtml
;
7715 this.paragraphize
.safes
= [];
7716 this.paragraphize
.z
= 0;
7719 html
= html
.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>');
7720 html
= html
.replace(/<\/pre>/gi, '</pre>\n\n');
7721 html
= html
.replace(/<p>\s<br><\/p>/gi, '<p></p>');
7723 html
= this.paragraphize
.getSafes(html
);
7725 html
= html
.replace('<br>', '\n');
7726 html
= this.paragraphize
.convert(html
);
7728 html
= this.paragraphize
.clear(html
);
7729 html
= this.paragraphize
.restoreSafes(html
);
7732 html
= html
.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts
.paragraphizeBlocks
.join(
7733 '|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');
7735 return $.trim(html
);
7737 getSafes: function (html
) {
7738 var $div
= $('<div />').append(html
);
7740 // remove paragraphs in blockquotes
7741 $div
.find('blockquote p').replaceWith(function () {
7742 return $(this).append('<br />').contents();
7745 $div
.find(this.opts
.paragraphizeBlocks
.join(', ')).each($.proxy(function (i
, s
) {
7746 this.paragraphize
.z
++;
7747 this.paragraphize
.safes
[this.paragraphize
.z
] = s
.outerHTML
;
7749 return $(s
).replaceWith('\n#####replace' + this.paragraphize
.z
+ '#####\n\n');
7753 // deal with redactor selection markers
7754 $div
.find('span.redactor-selection-marker').each($.proxy(function (i
, s
) {
7755 this.paragraphize
.z
++;
7756 this.paragraphize
.safes
[this.paragraphize
.z
] = s
.outerHTML
;
7758 return $(s
).replaceWith('\n#####replace' + this.paragraphize
.z
+ '#####\n\n');
7763 restoreSafes: function (html
) {
7764 $.each(this.paragraphize
.safes
, function (i
, s
) {
7765 s
= (typeof s
!== 'undefined') ? s
.replace(/\$/g, '$') : s
;
7766 html
= html
.replace('#####replace' + i
+ '#####', s
);
7772 convert: function (html
) {
7773 html
= html
.replace(/\r\n/g, 'xparagraphmarkerz');
7774 html
= html
.replace(/\n/g, 'xparagraphmarkerz');
7775 html
= html
.replace(/\r/g, 'xparagraphmarkerz');
7778 html
= html
.replace(re1
, ' ');
7779 html
= $.trim(html
);
7781 var re2
= /xparagraphmarkerzxparagraphmarkerz/gi;
7782 html
= html
.replace(re2
, '</p><p>');
7784 var re3
= /xparagraphmarkerz/gi;
7785 html
= html
.replace(re3
, '<br>');
7787 html
= '<p>' + html
+ '</p>';
7789 html
= html
.replace('<p></p>', '');
7790 html
= html
.replace('\r\n\r\n', '');
7791 html
= html
.replace(/<\/p><p>/g, '</p>\r\n\r\n<p>');
7792 html
= html
.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7793 html
= html
.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7794 html
= html
.replace(new RegExp('<p><br\\s?/?>', 'g'), '<p>');
7795 html
= html
.replace(new RegExp('<br\\s?/?></p>', 'g'), '</p>');
7796 html
= html
.replace(/<p> <\/p>/gi, '');
7797 html
= html
.replace(/<p>\s?<br> <\/p>/gi, '');
7798 html
= html
.replace(/<p>\s?<br>/gi, '<p>');
7802 clear: function (html
) {
7804 html
= html
.replace(
7805 /<p>(.*?)#####replace(.*?)#####\s?<\/p>/gi,
7806 '<p>$1</p>#####replace$2#####'
7808 html
= html
.replace(/(<br\s?\/?>){2,}<\/p>/gi, '</p>');
7810 html
= html
.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>');
7811 html
= html
.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>');
7812 html
= html
.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>');
7813 html
= html
.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>');
7815 html
= html
.replace(new RegExp('<p><p ', 'gi'), '<p ');
7816 html
= html
.replace(new RegExp('<p><p>', 'gi'), '<p>');
7817 html
= html
.replace(new RegExp('</p></p>', 'gi'), '</p>');
7818 html
= html
.replace(new RegExp('<p>\\s?</p>', 'gi'), '');
7819 html
= html
.replace(new RegExp('\n</p>', 'gi'), '</p>');
7820 html
= html
.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>');
7821 html
= html
.replace(new RegExp('<p>\t*</p>', 'gi'), '');
7829 paste: function () {
7831 init: function (e
) {
7832 this.rtePaste
= true;
7833 var pre
= (this.opts
.type
=== 'pre' || this.utils
.isCurrentOrParent('pre')) ? true : false;
7836 if (this.detect
.isDesktop()) {
7838 if (!this.paste
.pre
&& this.opts
.clipboardImageUpload
&& this.opts
.imageUpload
&& this.paste
.detectClipboardUpload(
7840 if (this.detect
.isIe()) {
7841 setTimeout($.proxy(this.paste
.clipboardUpload
, this),
7850 this.utils
.saveScroll();
7851 this.selection
.save();
7852 this.paste
.createPasteBox(pre
);
7854 $(window
).on('scroll.redactor-freeze', $.proxy(function () {
7855 $(window
).scrollTop(this.saveBodyScroll
);
7859 setTimeout($.proxy(function () {
7860 var html
= this.paste
.getPasteBoxCode(pre
);
7864 this.selection
.restore();
7866 this.utils
.restoreScroll();
7869 var data
= this.clean
.getCurrentType(html
);
7872 html
= this.clean
.onPaste(html
, data
);
7875 var returned
= this.core
.callback('paste', html
);
7876 html
= (typeof returned
=== 'undefined') ? html
: returned
;
7878 this.paste
.insert(html
, data
);
7879 this.rtePaste
= false;
7881 // clean pre breaklines
7883 this.clean
.cleanPre();
7886 $(window
).off('scroll.redactor-freeze');
7891 getPasteBoxCode: function (pre
) {
7892 var html
= (pre
) ? this.$pasteBox
.val() : this.$pasteBox
.html();
7893 this.$pasteBox
.remove();
7897 createPasteBox: function (pre
) {
7905 this.$pasteBox
= (pre
) ? $('<textarea>').css(css
) : $('<div>').attr('contenteditable',
7908 this.paste
.appendPasteBox();
7909 this.$pasteBox
.focus();
7911 appendPasteBox: function () {
7912 if (this.detect
.isIe()) {
7913 this.core
.box().append(this.$pasteBox
);
7917 var $visibleModals
= $('.modal-body:visible');
7918 if ($visibleModals
.length
> 0) {
7919 $visibleModals
.append(this.$pasteBox
);
7922 $('body').prepend(this.$pasteBox
);
7926 detectClipboardUpload: function (e
) {
7927 e
= e
.originalEvent
|| e
;
7929 var clipboard
= e
.clipboardData
;
7930 if (this.detect
.isIe() || this.detect
.isFirefox()) {
7934 // prevent safari fake url
7935 var types
= clipboard
.types
;
7936 if (types
.indexOf('public.tiff') !== -1) {
7941 if (!clipboard
.items
|| !clipboard
.items
.length
) {
7945 var file
= clipboard
.items
[0].getAsFile();
7946 if (file
=== null) {
7950 var reader
= new FileReader();
7951 reader
.readAsDataURL(file
);
7952 reader
.onload
= $.proxy(this.paste
.insertFromClipboard
, this);
7956 clipboardUpload: function () {
7957 var imgs
= this.$editor
.find('img');
7958 $.each(imgs
, $.proxy(function (i
, s
) {
7959 if (s
.src
.search(/^data\:image/i) === -1) {
7963 var formData
= !!window
.FormData
? new FormData() : null;
7964 if (!window
.FormData
) {
7968 this.upload
.direct
= true;
7969 this.upload
.type
= 'image';
7970 this.upload
.url
= this.opts
.imageUpload
;
7971 this.upload
.callback
= $.proxy(function (data
) {
7972 if (this.detect
.isIe()) {
7973 $(s
).wrap($('<figure />'));
7977 var $parent
= $(s
).parent();
7978 this.utils
.replaceToTag($parent
, 'figure');
7982 this.core
.callback('imageUpload', $(s
), data
);
7986 var blob
= this.utils
.dataURItoBlob(s
.src
);
7988 formData
.append('clipboard', 1);
7989 formData
.append(this.opts
.imageUploadParam
, blob
);
7991 this.upload
.send(formData
, false);
7993 this.rtePaste
= false;
7997 insertFromClipboard: function (e
) {
7998 var formData
= !!window
.FormData
? new FormData() : null;
7999 if (!window
.FormData
) {
8003 this.upload
.direct
= true;
8004 this.upload
.type
= 'image';
8005 this.upload
.url
= this.opts
.imageUpload
;
8006 this.upload
.callback
= this.image
.insert
;
8008 var blob
= this.utils
.dataURItoBlob(e
.target
.result
);
8010 formData
.append('clipboard', 1);
8011 formData
.append(this.opts
.imageUploadParam
, blob
);
8013 this.upload
.send(formData
, e
);
8014 this.rtePaste
= false;
8016 insert: function (html
, data
) {
8018 this.insert
.raw(html
);
8020 else if (data
.text
) {
8021 this.insert
.text(html
);
8024 this.insert
.html(html
, data
);
8027 // Firefox Clipboard Observe
8028 if (this.detect
.isFirefox() && this.opts
.imageUpload
&& this.opts
.clipboardImageUpload
) {
8029 setTimeout($.proxy(this.paste
.clipboardUpload
, this), 100);
8036 // =placeholder -- UNSUPPORTED MODULE
8037 placeholder: function () {
8039 enable: function () {},
8040 show: function () {},
8041 update: function () {},
8042 hide: function () {},
8044 init: function () {},
8045 enabled: function () {},
8046 enableEvents: function () {},
8047 disableEvents: function () {},
8048 build: function () {},
8049 buildPosition: function () {},
8050 getPosition: function () {},
8051 isEditorEmpty: function () {},
8052 isAttr: function () {},
8053 destroy: function () {}
8057 // =progress -- UNSUPPORTED MODULE
8058 progress: function () {
8062 target
: document
.body
, // or id selector
8063 show: function () {},
8064 hide: function () {},
8065 update: function () {},
8067 build: function () {},
8068 destroy: function () {}
8073 selection: function () {
8076 if (window
.getSelection
) {
8077 return window
.getSelection();
8079 else if (document
.selection
&& document
.selection
.type
!== 'Control') {
8080 return document
.selection
;
8085 range: function (sel
) {
8086 if (typeof sel
=== 'undefined') {
8087 sel
= this.selection
.get();
8090 if (sel
.getRangeAt
&& sel
.rangeCount
) {
8091 return sel
.getRangeAt(0);
8097 return (this.selection
.isCollapsed()) ? false : true;
8099 isRedactor: function () {
8100 var range
= this.selection
.range();
8102 if (range
!== null) {
8103 var el
= range
.startContainer
.parentNode
;
8105 if ($(el
).hasClass('redactor-in') || $(el
).parents('.redactor-in').length
!== 0) {
8112 isCollapsed: function () {
8113 var sel
= this.selection
.get();
8115 return (sel
=== null) ? false : sel
.isCollapsed
;
8117 update: function (sel
, range
) {
8118 if (range
=== null) {
8122 sel
.removeAllRanges();
8123 sel
.addRange(range
);
8125 current: function () {
8126 var sel
= this.selection
.get();
8128 return (sel
=== null) ? false : sel
.anchorNode
;
8130 parent: function () {
8131 var current
= this.selection
.current();
8133 return (current
=== null) ? false : current
.parentNode
;
8135 block: function (node
) {
8136 node
= node
|| this.selection
.current();
8139 if (this.utils
.isBlockTag(node
.tagName
)) {
8140 return ($(node
).hasClass('redactor-in')) ? false : node
;
8143 node
= node
.parentNode
;
8148 inline: function (node
) {
8149 node
= node
|| this.selection
.current();
8152 if (this.utils
.isInlineTag(node
.tagName
)) {
8153 return ($(node
).hasClass('redactor-in')) ? false : node
;
8156 node
= node
.parentNode
;
8161 element: function (node
) {
8163 node
= this.selection
.current();
8167 if (node
.nodeType
=== 1) {
8168 if ($(node
).hasClass('redactor-in')) {
8175 node
= node
.parentNode
;
8181 var current
= this.selection
.current();
8183 return (current
=== null) ? false : this.selection
.current().previousSibling
;
8186 var current
= this.selection
.current();
8188 return (current
=== null) ? false : this.selection
.current().nextSibling
;
8190 blocks: function (tag
) {
8192 var nodes
= this.selection
.nodes(tag
);
8194 $.each(nodes
, $.proxy(function (i
, node
) {
8195 if (this.utils
.isBlock(node
)) {
8201 var block
= this.selection
.block();
8202 if (blocks
.length
=== 0 && block
=== false) {
8205 else if (blocks
.length
=== 0 && block
!== false) {
8213 inlines: function (tag
) {
8215 var nodes
= this.selection
.nodes(tag
);
8217 $.each(nodes
, $.proxy(function (i
, node
) {
8218 if (this.utils
.isInline(node
)) {
8224 var inline
= this.selection
.inline();
8225 if (inlines
.length
=== 0 && inline
=== false) {
8228 else if (inlines
.length
=== 0 && inline
!== false) {
8235 nodes: function (tag
) {
8236 var filter
= (typeof tag
=== 'undefined') ? [] : (($.isArray(tag
)) ? tag
: [tag
]);
8238 var sel
= this.selection
.get();
8239 var range
= this.selection
.range(sel
);
8241 var resultNodes
= [];
8243 if (this.utils
.isCollapsed()) {
8244 nodes
= [this.selection
.current()];
8247 var node
= range
.startContainer
;
8248 var endNode
= range
.endContainer
;
8251 if (node
=== endNode
) {
8256 while (node
&& node
!== endNode
) {
8257 nodes
.push(node
= this.selection
.nextNode(node
));
8260 // partially selected nodes
8261 node
= range
.startContainer
;
8262 while (node
&& node
!== range
.commonAncestorContainer
) {
8263 nodes
.unshift(node
);
8264 node
= node
.parentNode
;
8268 // remove service nodes
8269 $.each(nodes
, function (i
, s
) {
8271 var tagName
= (s
.nodeType
!== 1) ? false : s
.tagName
.toLowerCase();
8273 if ($(s
).hasClass('redactor-script-tag') || $(s
).hasClass(
8274 'redactor-selection-marker')) {
8277 else if (tagName
&& filter
.length
!== 0 && $.inArray(tagName
,
8283 resultNodes
.push(s
);
8288 return (resultNodes
.length
=== 0) ? [] : resultNodes
;
8290 nextNode: function (node
) {
8291 if (node
.hasChildNodes()) {
8292 return node
.firstChild
;
8295 while (node
&& !node
.nextSibling
) {
8296 node
= node
.parentNode
;
8303 return node
.nextSibling
;
8307 this.marker
.insert();
8308 this.savedSel
= this.core
.editor().html();
8310 restore: function (removeMarkers
) {
8311 var node1
= this.marker
.find(1);
8312 var node2
= this.marker
.find(2);
8314 if (this.detect
.isFirefox()) {
8315 this.core
.editor().focus();
8318 if (node1
.length
!== 0 && node2
.length
!== 0) {
8319 this.caret
.set(node1
, node2
);
8321 else if (node1
.length
!== 0) {
8322 this.caret
.start(node1
);
8325 this.core
.editor().focus();
8328 if (removeMarkers
!== false) {
8329 this.marker
.remove();
8330 this.savedSel
= false;
8333 saveInstant: function () {
8334 var el
= this.core
.editor()[0];
8335 var doc
= el
.ownerDocument
, win
= doc
.defaultView
;
8336 var sel
= win
.getSelection();
8338 if (!sel
.getRangeAt
|| !sel
.rangeCount
) {
8342 var range
= sel
.getRangeAt(0);
8343 var selectionRange
= range
.cloneRange();
8345 selectionRange
.selectNodeContents(el
);
8346 selectionRange
.setEnd(range
.startContainer
, range
.startOffset
);
8348 var start
= selectionRange
.toString().length
;
8352 end
: start
+ range
.toString().length
,
8353 node
: range
.startContainer
8358 restoreInstant: function (saved
) {
8359 if (typeof saved
=== 'undefined' && !this.saved
) {
8363 this.saved
= (typeof saved
!== 'undefined') ? saved
: this.saved
;
8365 var $node
= this.core
.editor().find(this.saved
.node
);
8366 if ($node
.length
!== 0 && $node
.text().trim().replace(/\u200B/g,
8370 var range
= document
.createRange();
8371 range
.setStart($node
[0], 0);
8373 var sel
= window
.getSelection();
8374 sel
.removeAllRanges();
8375 sel
.addRange(range
);
8382 var el
= this.core
.editor()[0];
8383 var doc
= el
.ownerDocument
, win
= doc
.defaultView
;
8384 var charIndex
= 0, range
= doc
.createRange();
8386 range
.setStart(el
, 0);
8387 range
.collapse(true);
8389 var nodeStack
= [el
], node
, foundStart
= false, stop
= false;
8390 while (!stop
&& (node
= nodeStack
.pop())) {
8391 if (node
.nodeType
== 3) {
8392 var nextCharIndex
= charIndex
+ node
.length
;
8393 if (!foundStart
&& this.saved
.start
>= charIndex
&& this.saved
.start
<= nextCharIndex
) {
8394 range
.setStart(node
, this.saved
.start
- charIndex
);
8398 if (foundStart
&& this.saved
.end
>= charIndex
&& this.saved
.end
<= nextCharIndex
) {
8399 range
.setEnd(node
, this.saved
.end
- charIndex
);
8402 charIndex
= nextCharIndex
;
8405 var i
= node
.childNodes
.length
;
8407 nodeStack
.push(node
.childNodes
[i
]);
8412 var sel
= win
.getSelection();
8413 sel
.removeAllRanges();
8414 sel
.addRange(range
);
8416 node: function (node
) {
8417 $(node
).prepend(this.marker
.get(1));
8418 $(node
).append(this.marker
.get(2));
8420 this.selection
.restore();
8423 this.core
.editor().focus();
8425 var sel
= this.selection
.get();
8426 var range
= this.selection
.range(sel
);
8428 range
.selectNodeContents(this.core
.editor()[0]);
8430 this.selection
.update(sel
, range
);
8432 remove: function () {
8433 this.selection
.get().removeAllRanges();
8435 replace: function (html
) {
8436 this.insert
.html(html
);
8439 return this.selection
.get().toString();
8443 var sel
= this.selection
.get();
8445 if (sel
.rangeCount
) {
8446 var container
= document
.createElement('div');
8447 var len
= sel
.rangeCount
;
8448 for (var i
= 0; i
< len
; ++i
) {
8449 container
.appendChild(sel
.getRangeAt(i
).cloneContents());
8452 html
= this.clean
.onGet(container
.innerHTML
);
8457 extractEndOfNode: function (node
) {
8458 var sel
= this.selection
.get();
8459 var range
= this.selection
.range(sel
);
8461 var clonedRange
= range
.cloneRange();
8462 clonedRange
.selectNodeContents(node
);
8463 clonedRange
.setStart(range
.endContainer
, range
.endOffset
);
8465 return clonedRange
.extractContents();
8469 removeMarkers: function () {
8470 this.marker
.remove();
8472 marker: function (num
) {
8473 return this.marker
.get(num
);
8475 markerHtml: function (num
) {
8476 return this.marker
.html(num
);
8483 shortcuts: function () {
8485 // based on https://github.com/jeresig/jquery.hotkeys
8486 hotkeysSpecialKeys
: {
8573 init: function (e
, key
) {
8574 // disable browser's hot keys for bold and italic if shortcuts off
8575 if (this.opts
.shortcuts
=== false) {
8576 if ((e
.ctrlKey
|| e
.metaKey
) && (key
=== 66 || key
=== 73)) {
8584 $.each(this.opts
.shortcuts
, $.proxy(function (str
, command
) {
8585 this.shortcuts
.build(e
, str
, command
);
8590 build: function (e
, str
, command
) {
8591 var handler
= $.proxy(function () {
8592 this.shortcuts
.buildHandler(command
);
8596 var keys
= str
.split(',');
8597 var len
= keys
.length
;
8598 for (var i
= 0; i
< len
; i
++) {
8599 if (typeof keys
[i
] === 'string') {
8600 this.shortcuts
.handler(e
, $.trim(keys
[i
]), handler
);
8605 buildHandler: function (command
) {
8607 if (command
.func
.search(/\./) !== '-1') {
8608 func
= command
.func
.split('.');
8609 if (typeof this[func
[0]] !== 'undefined') {
8610 this[func
[0]][func
[1]].apply(this, command
.params
);
8614 this[command
.func
].apply(this, command
.params
);
8617 handler: function (e
, keys
, origHandler
) {
8618 keys
= keys
.toLowerCase().split(' ');
8620 var special
= this.shortcuts
.hotkeysSpecialKeys
[e
.keyCode
];
8621 var character
= String
.fromCharCode(e
.which
).toLowerCase();
8622 var modif
= '', possible
= {};
8624 $.each(['alt', 'ctrl', 'meta', 'shift'], function (index
, specialKey
) {
8625 if (e
[specialKey
+ 'Key'] && special
!== specialKey
) {
8626 modif
+= specialKey
+ '+';
8631 possible
[modif
+ special
] = true;
8635 possible
[modif
+ character
] = true;
8636 possible
[modif
+ this.shortcuts
.hotkeysShiftNums
[character
]] = true;
8638 // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
8639 if (modif
=== 'shift+') {
8640 possible
[this.shortcuts
.hotkeysShiftNums
[character
]] = true;
8644 var len
= keys
.length
;
8645 for (var i
= 0; i
< len
; i
++) {
8646 if (possible
[keys
[i
]]) {
8648 return origHandler
.apply(this, arguments
);
8655 // =storage -- UNSUPPORTED MODULE
8656 storage: function () {
8659 add: function () {},
8660 status: function () {},
8661 observe: function () {},
8662 changes: function () {}
8668 toolbar: function () {
8670 build: function () {
8671 this.button
.hideButtons();
8672 this.button
.hideButtonsOnMobile();
8674 this.$toolbarBox
= $('<div class="redactor-toolbar-box" />');
8675 this.$toolbarBox
[0].innerHTML
= '<ul class="redactor-toolbar" id="redactor-toolbar-' + this.uuid
+ '" role="toolbar"></ul>';
8676 this.$toolbar
= $(this.$toolbarBox
[0].children
[0]);
8677 this.$box
[0].insertBefore(this.$toolbarBox
[0], this.$box
[0].firstChild
);
8679 this.button
.$toolbar
= this.$toolbar
;
8680 this.button
.setFormatting();
8681 this.button
.load(this.$toolbar
);
8683 require(['Core'], (function(Core
) {
8684 this.$toolbar
[0].addEventListener('keydown', this.toolbar
.keydown
.bind(this, Core
));
8687 createContainer: function () {},
8688 append: function () {},
8689 setOverflow: function () {},
8690 setFixed: function () {},
8691 setUnfixed: function () {},
8692 getBoxTop: function () {},
8693 observeScroll: function () {},
8694 observeScrollResize: function () {},
8695 observeScrollEnable: function () {},
8696 observeScrollDisable: function () {},
8697 setDropdownsFixed: function () {},
8698 unsetDropdownsFixed: function () {},
8699 setDropdownPosition: function () {},
8701 * @param {object} Core
8702 * @param {KeyboardEvent} event
8704 keydown: function(Core
, event
) {
8705 var activeButton
= document
.activeElement
;
8706 if (!activeButton
.classList
.contains('re-button')) {
8710 // Enter, Space, End, Home, ArrowLeft, ArrowRight, ArrowDown
8711 // Remarks: ArrowUp is not considered, because we do not support radio groups at the top level.
8712 var keyboardCodes
= [13, 32, 35, 36, 37, 39, 40];
8713 if (keyboardCodes
.indexOf(event
.which
) === -1) {
8717 // [Enter] || [Space]
8718 if (event
.which
=== 13 || event
.which
=== 32) {
8719 event
.preventDefault();
8721 require(['Core'], function(Core
) {
8722 Core
.triggerEvent(activeButton
, 'mousedown');
8728 // [ArrowDown] opens drop-down menus, but does nothing on "regular" buttons.
8729 if (event
.which
=== 40) {
8730 if (elAttr(activeButton
, 'aria-haspopup') !== 'true') {
8734 event
.preventDefault();
8735 Core
.triggerEvent(activeButton
, 'mousedown');
8737 var dropdown
= $(activeButton
).data('dropdown');
8738 var firstItem
= elBySel('li', dropdown
[0]);
8739 if (firstItem
) firstItem
.focus();
8743 event
.preventDefault();
8745 var buttons
= Array
.prototype.slice
.call(elBySelAll('.re-button', this.$toolbar
[0]));
8746 var newActiveButton
= null;
8748 if (event
.which
=== 35) {
8749 newActiveButton
= buttons
[buttons
.length
- 1];
8752 else if (event
.which
=== 36) {
8753 newActiveButton
= buttons
[0];
8756 var index
= buttons
.indexOf(activeButton
);
8759 if (event
.which
=== 37) {
8763 index
= buttons
.length
- 1;
8767 else if (event
.which
=== 39) {
8770 if (index
=== buttons
.length
) {
8775 newActiveButton
= buttons
[index
];
8778 if (newActiveButton
!== null) {
8779 newActiveButton
.focus();
8785 // =upload -- UNSUPPORTED MODULE
8786 upload: function () {
8788 init: function () {},
8789 directUpload: function () {},
8790 onDrop: function () {},
8791 traverseFile: function () {},
8792 setConfig: function () {},
8793 getType: function () {},
8794 getHiddenFields: function () {},
8795 send: function () {},
8796 onDrag: function () {},
8797 onDragLeave: function () {},
8798 clearImageFields: function () {},
8799 addImageFields: function () {},
8800 removeImageFields: function () {},
8801 clearFileFields: function () {},
8802 addFileFields: function () {},
8803 removeFileFields: function () {}
8807 // =s3 -- UNSUPPORTED MODULE
8808 uploads3: function () {
8810 send: function () {},
8811 executeOnSignedUrl: function () {},
8812 createCORSRequest: function () {},
8813 sendToS3: function () {}
8818 utils: function () {
8820 isEmpty: function (html
) {
8821 html
= (typeof html
=== 'undefined') ? this.core
.editor().html() : html
;
8823 html
= html
.replace(/[\u200B-\u200D\uFEFF]/g, '');
8824 html
= html
.replace(/ /gi, '');
8825 html
= html
.replace(/<\/?br\s?\/?>/g, '');
8826 html
= html
.replace(/\s/g, '');
8827 html
= html
.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
8828 html
= html
.replace(/<iframe(.*?[^>])>$/i, 'iframe');
8829 html
= html
.replace(/<source(.*?[^>])>$/i, 'source');
8831 // remove empty tags
8832 html
= html
.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8833 html
= html
.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
8835 html
= $.trim(html
);
8839 isElement: function (obj
) {
8841 // Using W3 DOM2 (works for FF, Opera and Chrome)
8842 return obj
instanceof HTMLElement
;
8845 return (typeof obj
=== 'object') && (obj
.nodeType
=== 1) && (typeof obj
.style
=== 'object') && (typeof obj
.ownerDocument
=== 'object');
8848 strpos: function (haystack
, needle
, offset
) {
8849 var i
= haystack
.indexOf(needle
, offset
);
8850 return i
>= 0 ? i
: false;
8852 dataURItoBlob: function (dataURI
) {
8854 if (dataURI
.split(',')[0].indexOf('base64') >= 0) {
8855 byteString
= atob(dataURI
.split(',')[1]);
8858 byteString
= unescape(dataURI
.split(',')[1]);
8861 var mimeString
= dataURI
.split(',')[0].split(':')[1].split(';')[0];
8863 var ia
= new Uint8Array(byteString
.length
);
8864 for (var i
= 0; i
< byteString
.length
; i
++) {
8865 ia
[i
] = byteString
.charCodeAt(i
);
8868 return new Blob([ia
], {type
: mimeString
});
8870 getOuterHtml: function (el
) {
8871 return $('<div>').append($(el
).eq(0).clone()).html();
8873 cloneAttributes: function (from, to
) {
8874 from = from[0] || from;
8877 var attrs
= from.attributes
;
8878 var len
= attrs
.length
;
8880 var attr
= attrs
[len
];
8881 to
.attr(attr
.name
, attr
.value
);
8886 breakBlockTag: function () {
8887 var block
= this.selection
.block();
8892 var isEmpty
= this.utils
.isEmpty(block
.innerHTML
);
8894 var tag
= block
.tagName
.toLowerCase();
8895 if (tag
=== 'pre' || tag
=== 'li' || tag
=== 'td' || tag
=== 'th') {
8899 if (!isEmpty
&& this.utils
.isStartOfElement(block
)) {
8902 $next
: $(block
).next(),
8906 else if (!isEmpty
&& this.utils
.isEndOfElement(block
)) {
8909 $next
: $(block
).next(),
8914 var endOfNode
= this.selection
.extractEndOfNode(block
);
8915 var $nextPart
= $('<' + tag
+ ' />').append(endOfNode
);
8917 $nextPart
= this.utils
.cloneAttributes(block
, $nextPart
);
8918 $(block
).after($nextPart
);
8927 inBlocks: function (tags
) {
8928 tags
= ($.isArray(tags
)) ? tags
: [tags
];
8930 var blocks
= this.selection
.blocks();
8931 var len
= blocks
.length
;
8932 var contains
= false;
8933 for (var i
= 0; i
< len
; i
++) {
8934 if (blocks
[i
] !== false) {
8935 var tag
= blocks
[i
].tagName
.toLowerCase();
8937 if ($.inArray(tag
, tags
) !== -1) {
8946 inInlines: function (tags
) {
8947 tags
= ($.isArray(tags
)) ? tags
: [tags
];
8949 var inlines
= this.selection
.inlines();
8950 var len
= inlines
.length
;
8951 var contains
= false;
8952 for (var i
= 0; i
< len
; i
++) {
8953 var tag
= inlines
[i
].tagName
.toLowerCase();
8955 if ($.inArray(tag
, tags
) !== -1) {
8963 isTag: function (current
, tag
) {
8964 var element
= $(current
).closest(tag
, this.core
.editor()[0]);
8965 if (element
.length
=== 1) {
8971 isBlock: function (block
) {
8972 if (block
=== null) {
8976 block
= block
[0] || block
;
8978 return block
&& this.utils
.isBlockTag(block
.tagName
);
8980 isBlockTag: function (tag
) {
8981 return (typeof tag
=== 'undefined') ? false : this.reIsBlock
.test(tag
);
8983 isInline: function (inline
) {
8984 inline
= inline
[0] || inline
;
8986 return inline
&& this.utils
.isInlineTag(inline
.tagName
);
8988 isInlineTag: function (tag
) {
8989 return (typeof tag
=== 'undefined') ? false : this.reIsInline
.test(tag
);
8990 }, // parents detection
8991 isRedactorParent: function (el
) {
8996 if ($(el
).parents('.redactor-in').length
=== 0 || $(el
).hasClass('redactor-in')) {
9002 isCurrentOrParentHeader: function () {
9003 return this.utils
.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
9005 isCurrentOrParent: function (tagName
) {
9006 var parent
= this.selection
.parent();
9007 var current
= this.selection
.current();
9009 if ($.isArray(tagName
)) {
9011 $.each(tagName
, $.proxy(function (i
, s
) {
9012 if (this.utils
.isCurrentOrParentOne(current
, parent
, s
)) {
9017 return (matched
=== 0) ? false : true;
9020 return this.utils
.isCurrentOrParentOne(current
, parent
, tagName
);
9023 isCurrentOrParentOne: function (current
, parent
, tagName
) {
9024 tagName
= tagName
.toUpperCase();
9026 return parent
&& parent
.tagName
=== tagName
? parent
: current
&& current
.tagName
=== tagName
? current
: false;
9028 isEditorRelative: function () {
9029 var position
= this.core
.editor().css('position');
9030 var arr
= ['absolute', 'fixed', 'relative'];
9032 return ($.inArray(arr
, position
) !== -1);
9034 setEditorRelative: function () {
9035 this.core
.editor().addClass('redactor-relative');
9037 getScrollTarget: function () {
9038 var $scrollTarget
= $(this.opts
.scrollTarget
);
9040 return ($scrollTarget
.length
!== 0) ? $scrollTarget
: $(document
);
9042 freezeScroll: function () {
9043 this.freezeScrollTop
= this.utils
.getScrollTarget().scrollTop();
9044 this.utils
.getScrollTarget().scrollTop(this.freezeScrollTop
);
9046 unfreezeScroll: function () {
9047 if (typeof this.freezeScrollTop
=== 'undefined') {
9051 this.utils
.getScrollTarget().scrollTop(this.freezeScrollTop
);
9053 saveScroll: function () {
9054 this.tmpScrollTop
= this.utils
.getScrollTarget().scrollTop();
9056 restoreScroll: function () {
9057 if (typeof this.tmpScrollTop
=== 'undefined') {
9061 this.utils
.getScrollTarget().scrollTop(this.tmpScrollTop
);
9063 isStartOfElement: function (element
) {
9064 if (typeof element
=== 'undefined') {
9065 element
= this.selection
.block();
9071 return (this.offset
.get(element
) === 0) ? true : false;
9073 isEndOfElement: function (element
) {
9074 if (typeof element
=== 'undefined') {
9075 element
= this.selection
.block();
9081 var text
= $.trim($(element
).text()).replace(/[\t\n\r\n]/g, '').replace(/\u200B/g,
9084 var offset
= this.offset
.get(element
);
9086 return (offset
=== text
.length
) ? true : false;
9088 removeEmptyAttr: function (el
, attr
) {
9090 if (typeof $el
.attr(attr
) === 'undefined') {
9094 if ($el
.attr(attr
) === '') {
9095 $el
.removeAttr(attr
);
9101 replaceToTag: function (node
, tag
) {
9103 $(node
).replaceWith(function () {
9104 replacement
= $('<' + tag
+ ' />').append($(this).contents());
9106 for (var i
= 0; i
< this.attributes
.length
; i
++) {
9107 replacement
.attr(this.attributes
[i
].name
,
9108 this.attributes
[i
].value
9117 isSelectAll: function () {
9118 return this.selectAll
;
9120 enableSelectAll: function () {
9121 this.selectAll
= true;
9123 disableSelectAll: function () {
9124 this.selectAll
= false;
9126 disableBodyScroll: function () {},
9127 measureScrollbar: function () {
9128 var $body
= $('body');
9129 var scrollDiv
= document
.createElement('div');
9130 scrollDiv
.className
= 'redactor-scrollbar-measure';
9132 $body
.append(scrollDiv
);
9133 var scrollbarWidth
= scrollDiv
.offsetWidth
- scrollDiv
.clientWidth
;
9134 $body
[0].removeChild(scrollDiv
);
9135 return scrollbarWidth
;
9137 enableBodyScroll: function () {},
9138 appendFields: function (appendFields
, data
) {
9139 if (!appendFields
) {
9142 else if (typeof appendFields
=== 'object') {
9143 $.each(appendFields
, function (k
, v
) {
9144 if (v
!== null && v
.toString().indexOf('#') === 0) {
9155 var $fields
= $(appendFields
);
9156 if ($fields
.length
=== 0) {
9161 $fields
.each(function () {
9162 data
.append($(this).attr('name'), $(this).val());
9168 appendForms: function (appendForms
, data
) {
9173 var $forms
= $(appendForms
);
9174 if ($forms
.length
=== 0) {
9178 var formData
= $forms
.serializeArray();
9180 $.each(formData
, function (z
, f
) {
9181 data
.append(f
.name
, f
.value
);
9187 isRgb: function (str
) {
9188 return (str
.search(/^rgb/i) === 0);
9190 rgb2hex: function (rgb
) {
9192 /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
9194 return (rgb
&& rgb
.length
=== 4) ? '#' + ('0' + parseInt(rgb
[1],
9196 ).toString(16)).slice(-2) + ('0' + parseInt(rgb
[2],
9198 ).toString(16)).slice(-2) + ('0' + parseInt(rgb
[3],
9200 ).toString(16)).slice(-2) : '';
9204 isCollapsed: function () {
9205 return this.selection
.isCollapsed();
9207 isMobile: function () {
9208 return this.detect
.isMobile();
9210 isDesktop: function () {
9211 return this.detect
.isDesktop();
9213 isPad: function () {
9214 return this.detect
.isIpad();
9221 browser: function () {
9223 webkit: function () {
9224 return this.detect
.isWebkit();
9227 return this.detect
.isFirefox();
9230 return this.detect
.isIe();
9236 $(window
).on('load.tools.redactor', function () {
9237 $('[data-tools="redactor"]').redactor();
9241 Redactor
.prototype.init
.prototype = Redactor
.prototype;
9246 $.fn
.redactorAnimation = function (animation
, options
, callback
) {
9247 return this.each(function () {
9248 new redactorAnimation(this, animation
, options
, callback
);
9252 function redactorAnimation(element
, animation
, options
, callback
) {
9258 prefix
: 'redactor-',
9262 this.animation
= animation
;
9263 this.slide
= (this.animation
=== 'slideDown' || this.animation
=== 'slideUp');
9264 this.$element
= $(element
);
9265 this.prefixes
= ['', '-moz-', '-o-animation-', '-webkit-'];
9268 // options or callback
9269 if (typeof options
=== 'function') {
9274 this.opts
= $.extend(opts
, options
);
9279 this.$element
.height(this.$element
.height());
9283 this.init(callback
);
9287 redactorAnimation
.prototype = {
9289 init: function (callback
) {
9290 this.queue
.push(this.animation
);
9294 if (this.animation
=== 'show') {
9295 this.opts
.timing
= 'linear';
9296 this.$element
.removeClass('hide').show();
9298 if (typeof callback
=== 'function') {
9302 else if (this.animation
=== 'hide') {
9303 this.opts
.timing
= 'linear';
9304 this.$element
.hide();
9306 if (typeof callback
=== 'function') {
9311 this.animate(callback
);
9315 animate: function (callback
) {
9316 this.$element
.addClass('redactor-animated').css('display', '').removeClass('hide');
9317 this.$element
.addClass(this.opts
.prefix
+ this.queue
[0]);
9319 this.set(this.opts
.duration
+ 's', this.opts
.delay
+ 's', this.opts
.iterate
, this.opts
.timing
);
9321 var _callback
= (this.queue
.length
> 1) ? null : callback
;
9322 this.complete('AnimationEnd', $.proxy(function () {
9323 if (this.$element
.hasClass(this.opts
.prefix
+ this.queue
[0])) {
9327 if (this.queue
.length
) {
9328 this.animate(callback
);
9332 }, this), _callback
);
9334 set: function (duration
, delay
, iterate
, timing
) {
9335 var len
= this.prefixes
.length
;
9338 this.$element
.css(this.prefixes
[len
] + 'animation-duration', duration
);
9339 this.$element
.css(this.prefixes
[len
] + 'animation-delay', delay
);
9340 this.$element
.css(this.prefixes
[len
] + 'animation-iteration-count', iterate
);
9341 this.$element
.css(this.prefixes
[len
] + 'animation-timing-function', timing
);
9345 clean: function () {
9346 this.$element
.removeClass('redactor-animated');
9347 this.$element
.removeClass(this.opts
.prefix
+ this.queue
[0]);
9349 this.set('', '', '', '');
9352 complete: function (type
, make
, callback
) {
9353 this.$element
.one(type
.toLowerCase() + ' webkit' + type
+ ' o' + type
+ ' MS' + type
,
9354 $.proxy(function () {
9355 if (typeof make
=== 'function') {
9359 if (typeof callback
=== 'function') {
9372 if ($.inArray(this.animation
, effects
) !== -1) {
9373 this.$element
.css('display', 'none');
9378 this.$element
.css('height', '');