app/assets/javascripts/annex/jquery.redactor.js in annex-cms-0.3.6 vs app/assets/javascripts/annex/jquery.redactor.js in annex-cms-0.3.7

- old
+ new

@@ -1,12 +1,12 @@ /* - Redactor v10.0.2 - Updated: October 12, 2014 + Redactor v10.0.6 + Updated: January 7, 2015 http://imperavi.com/redactor/ - Copyright (c) 2009-2014, Imperavi LLC. + Copyright (c) 2009-2015, Imperavi LLC. License: http://imperavi.com/redactor/license/ Usage: $('#content').redactor(); */ @@ -92,15 +92,17 @@ return new Redactor.prototype.init(el, options); } // Functionality $.Redactor = Redactor; - $.Redactor.VERSION = '10.0.2'; - $.Redactor.modules = ['core', 'build', 'lang', 'toolbar', 'button', 'dropdown', 'code', - 'clean', 'tidy', 'paragraphize', 'tabifier', 'focus', 'placeholder', 'autosave', 'buffer', 'indent', 'alignment', 'paste', - 'keydown', 'keyup', 'shortcuts', 'line', 'list', 'block', 'inline', 'insert', 'caret', 'selection', 'observe', - 'link', 'image', 'file', 'modal', 'progress', 'upload', 'utils']; + $.Redactor.VERSION = '10.0.6'; + $.Redactor.modules = ['alignment', 'autosave', 'block', 'buffer', 'build', 'button', + 'caret', 'clean', 'code', 'core', 'dropdown', 'file', 'focus', + 'image', 'indent', 'inline', 'insert', 'keydown', 'keyup', + 'lang', 'line', 'link', 'list', 'modal', 'observe', 'paragraphize', + 'paste', 'placeholder', 'progress', 'selection', 'shortcuts', + 'tabifier', 'tidy', 'toolbar', 'upload', 'utils']; $.Redactor.opts = { // settings lang: 'en', @@ -164,11 +166,11 @@ convertImageLinks: true, convertVideoLinks: true, preSpaces: 4, // or false tabAsSpaces: false, // true or number of spaces - tabFocus: true, + tabKey: true, scrollTarget: false, toolbar: true, toolbarFixed: true, @@ -339,10 +341,11 @@ TAB: 9, CTRL: 17, META: 91, SHIFT: 16, ALT: 18, + RIGHT: 39, LEFT: 37, LEFT_WIN: 91 }, // Initialization @@ -421,100 +424,701 @@ { this[module][methods[z]] = this[module][methods[z]].bind(this); } }, - core: function() + alignment: function() { return { - getObject: function() + left: function() { - return $.extend({}, this); + this.alignment.set(''); }, - getEditor: function() + right: function() { - return this.$editor; + this.alignment.set('right'); }, - getBox: function() + center: function() { - return this.$box; + this.alignment.set('center'); }, - getElement: function() + justify: function() { - return this.$element; + this.alignment.set('justify'); }, - getTextarea: function() + set: function(type) { - return this.$textarea; + if (!this.utils.browser('msie')) this.$editor.focus(); + + this.buffer.set(); + this.selection.save(); + + this.alignment.blocks = this.selection.getBlocks(); + if (this.opts.linebreaks && this.alignment.blocks[0] === false) + { + this.alignment.setText(type); + } + else + { + this.alignment.setBlocks(type); + } + + this.selection.restore(); + this.code.sync(); }, - getToolbar: function() + setText: function(type) { - return (this.$toolbar) ? this.$toolbar : false; + var wrapper = this.selection.wrap('div'); + $(wrapper).attr('data-tagblock', 'redactor'); + $(wrapper).css('text-align', type); }, - addEvent: function(name) + setBlocks: function(type) { - this.core.event = name; + $.each(this.alignment.blocks, $.proxy(function(i, el) + { + var $el = this.utils.getAlignmentElement(el); + + if (!$el) return; + + if (type === '' && typeof($el.data('tagblock')) !== 'undefined') + { + $el.replaceWith($el.html()); + } + else + { + $el.css('text-align', type); + this.utils.removeEmptyAttr($el, 'style'); + } + + + }, this)); + } + }; + }, + autosave: function() + { + return { + enable: function() + { + if (!this.opts.autosave) return; + + this.autosave.html = false; + this.autosave.name = (this.opts.autosaveName) ? this.opts.autosaveName : this.$textarea.attr('name'); + + if (!this.opts.autosaveOnChange) + { + this.autosaveInterval = setInterval($.proxy(this.autosave.load, this), this.opts.autosaveInterval * 1000); + } }, - getEvent: function() + onChange: function() { - return this.core.event; + if (!this.opts.autosaveOnChange) return; + + this.autosave.load(); }, - setCallback: function(type, e, data) + load: function() { - var callback = this.opts[type + 'Callback']; - if ($.isFunction(callback)) + var html = this.code.get(); + if (this.autosave.html === html) return; + if (this.utils.isEmpty(html)) return; + + $.ajax({ + url: this.opts.autosave, + type: 'post', + data: 'name=' + this.autosave.name + '&' + this.autosave.name + '=' + escape(encodeURIComponent(html)), + success: $.proxy(function(data) + { + this.autosave.success(data, html); + + }, this) + }); + }, + success: function(data, html) + { + var json; + try { - return (typeof data == 'undefined') ? callback.call(this, e) : callback.call(this, e, data); + json = $.parseJSON(data); } + catch(e) + { + //data has already been parsed + json = data; + } + + var callbackName = (typeof json.error == 'undefined') ? 'autosave' : 'autosaveError'; + + this.core.setCallback(callbackName, this.autosave.name, json); + this.autosave.html = html; + }, + disable: function() + { + clearInterval(this.autosaveInterval); + } + }; + }, + block: function() + { + return { + formatting: function(name) + { + this.block.clearStyle = false; + var type, value; + + if (typeof this.formatting[name].data != 'undefined') type = 'data'; + else if (typeof this.formatting[name].attr != 'undefined') type = 'attr'; + else if (typeof this.formatting[name].class != 'undefined') type = 'class'; + + if (typeof this.formatting[name].clear != 'undefined') + { + this.block.clearStyle = true; + } + + if (type) value = this.formatting[name][type]; + + this.block.format(this.formatting[name].tag, type, value); + + }, + format: function(tag, type, value) + { + if (tag == 'quote') tag = 'blockquote'; + + var formatTags = ['p', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + if ($.inArray(tag, formatTags) == -1) return; + + this.block.isRemoveInline = (tag == 'pre' || tag.search(/h[1-6]/i) != -1); + + // focus + if (!this.utils.browser('msie')) this.$editor.focus(); + + this.block.blocks = this.selection.getBlocks(); + + this.block.blocksSize = this.block.blocks.length; + this.block.type = type; + this.block.value = value; + + this.buffer.set(); + this.selection.save(); + + this.block.set(tag); + + this.selection.restore(); + this.code.sync(); + + }, + set: function(tag) + { + this.selection.get(); + this.block.containerTag = this.range.commonAncestorContainer.tagName; + + if (this.range.collapsed) + { + this.block.setCollapsed(tag); + } else { - return (typeof data == 'undefined') ? e : data; + this.block.setMultiple(tag); } }, - destroy: function() + setCollapsed: function(tag) { - this.core.setCallback('destroy'); + var block = this.block.blocks[0]; + if (block === false) return; - // off events and remove data - this.$element.off('.redactor').removeData('redactor'); - this.$editor.off('.redactor'); + if (block.tagName == 'LI') + { + if (tag != 'blockquote') return; - // common - this.$editor.removeClass('redactor-editor redactor-linebreaks redactor-placeholder'); - this.$editor.removeAttr('contenteditable'); + this.block.formatListToBlockquote(); + return; + } - var html = this.code.get(); + var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); + if (isContainerTable && !this.opts.linebreaks) + { - if (this.build.isTextarea()) + document.execCommand('formatblock', false, '<' + tag + '>'); + + block = this.selection.getBlock(); + this.block.toggle($(block)); + + } + else if (block.tagName.toLowerCase() != tag) { - this.$box.after(this.$element); - this.$box.remove(); - this.$element.val(html).show(); + if (this.opts.linebreaks && tag == 'p') + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else + { + var $formatted = this.utils.replaceToTag(block, tag); + + this.block.toggle($formatted); + + if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove(); + if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted); + if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap(); + + this.block.formatTableWrapping($formatted); + } } + else if (tag == 'blockquote' && block.tagName.toLowerCase() == tag) + { + // blockquote off + if (this.opts.linebreaks) + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else + { + var $el = this.utils.replaceToTag(block, 'p'); + this.block.toggle($el); + } + } + else if (block.tagName.toLowerCase() == tag) + { + this.block.toggle($(block)); + } + + }, + setMultiple: function(tag) + { + var block = this.block.blocks[0]; + var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); + + if (block !== false && this.block.blocksSize === 1) + { + if (block.tagName.toLowerCase() == tag && tag == 'blockquote') + { + // blockquote off + if (this.opts.linebreaks) + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else + { + var $el = this.utils.replaceToTag(block, 'p'); + this.block.toggle($el); + } + } + else if (block.tagName == 'LI') + { + if (tag != 'blockquote') return; + + this.block.formatListToBlockquote(); + } + else if (this.block.containerTag == 'BLOCKQUOTE') + { + this.block.formatBlockquote(tag); + } + else if (this.opts.linebreaks && ((isContainerTable) || (this.range.commonAncestorContainer != block))) + { + this.block.formatWrap(tag); + } + else + { + if (this.opts.linebreaks && tag == 'p') + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else if (block.tagName === 'TD') + { + this.block.formatWrap(tag); + } + else + { + var $formatted = this.utils.replaceToTag(block, tag); + + this.block.toggle($formatted); + + if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted); + if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap(); + } + } + } else { - this.$box.after(this.$editor); - this.$box.remove(); - this.$element.html(html).show(); + if (this.opts.linebreaks || tag != 'p') + { + if (tag == 'blockquote') + { + var count = 0; + for (var i = 0; i < this.block.blocksSize; i++) + { + if (this.block.blocks[i].tagName == 'BLOCKQUOTE') count++; + } + + // only blockquote selected + if (count == this.block.blocksSize) + { + $.each(this.block.blocks, $.proxy(function(i,s) + { + if (this.opts.linebreaks) + { + $(s).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(s); + } + else + { + this.utils.replaceToTag(s, 'p'); + } + + }, this)); + + return; + } + + } + + this.block.formatWrap(tag); + } + else + { + var classSize = 0; + var toggleType = false; + if (this.block.type == 'class') + { + toggleType = 'toggle'; + classSize = $(this.block.blocks).filter('.' + this.block.value).size(); + + if (this.block.blocksSize == classSize) toggleType = 'toggle'; + else if (this.block.blocksSize > classSize) toggleType = 'set'; + else if (classSize === 0) toggleType = 'set'; + + } + + var exceptTags = ['ul', 'ol', 'li', 'td', 'th', 'dl', 'dt', 'dd']; + $.each(this.block.blocks, $.proxy(function(i,s) + { + if ($.inArray(s.tagName.toLowerCase(), exceptTags) != -1) return; + + var $formatted = this.utils.replaceToTag(s, tag); + + if (toggleType) + { + if (toggleType == 'toggle') this.block.toggle($formatted); + else if (toggleType == 'remove') this.block.remove($formatted); + else if (toggleType == 'set') this.block.setForce($formatted); + } + else this.block.toggle($formatted); + + if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove(); + if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted); + if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap(); + + + }, this)); + } } + }, + setForce: function($el) + { + // remove style and class if the specified setting + if (this.block.clearStyle) + { + $el.removeAttr('class').removeAttr('style'); + } - // paste box - if (this.$pasteBox) this.$pasteBox.remove(); + if (this.block.type == 'class') + { + $el.addClass(this.block.value); + return; + } + else if (this.block.type == 'attr' || this.block.type == 'data') + { + $el.attr(this.block.value.name, this.block.value.value); + return; + } + }, + toggle: function($el) + { + // remove style and class if the specified setting + if (this.block.clearStyle) + { + $el.removeAttr('class').removeAttr('style'); + } - // modal - if (this.$modalBox) this.$modalBox.remove(); - if (this.$modalOverlay) this.$modalOverlay.remove(); + if (this.block.type == 'class') + { + $el.toggleClass(this.block.value); + return; + } + else if (this.block.type == 'attr' || this.block.type == 'data') + { + if ($el.attr(this.block.value.name) == this.block.value.value) + { + $el.removeAttr(this.block.value.name); + } + else + { + $el.attr(this.block.value.name, this.block.value.value); + } - // buttons tooltip - $('.redactor-toolbar-tooltip').remove(); + return; + } + else + { + $el.removeAttr('style class'); + return; + } + }, + remove: function($el) + { + $el.removeClass(this.block.value); + }, + formatListToBlockquote: function() + { + var block = $(this.block.blocks[0]).closest('ul, ol'); - // autosave - clearInterval(this.autosaveInterval); + $(block).find('ul, ol').contents().unwrap(); + $(block).find('li').append($('<br>')).contents().unwrap(); + var $el = this.utils.replaceToTag(block, 'blockquote'); + this.block.toggle($el); + }, + formatBlockquote: function(tag) + { + document.execCommand('outdent'); + document.execCommand('formatblock', false, tag); + + this.clean.clearUnverified(); + this.$editor.find('p:empty').remove(); + + var formatted = this.selection.getBlock(); + + if (tag != 'p') + { + $(formatted).find('img').remove(); + } + + if (!this.opts.linebreaks) + { + this.block.toggle($(formatted)); + } + + this.$editor.find('ul, ol, tr, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); + + if (this.opts.linebreaks && tag == 'p') + { + this.utils.replaceWithContents(formatted); + } + + }, + formatWrap: function(tag) + { + if (this.block.containerTag == 'UL' || this.block.containerTag == 'OL') + { + if (tag == 'blockquote') + { + this.block.formatListToBlockquote(); + } + else + { + return; + } + } + + var formatted = this.selection.wrap(tag); + if (formatted === false) return; + + var $formatted = $(formatted); + + this.block.formatTableWrapping($formatted); + + var $elements = $formatted.find(this.opts.blockLevelElements.join(',') + ', td, table, thead, tbody, tfoot, th, tr'); + + if ((this.opts.linebreaks && tag == 'p') || tag == 'pre' || tag == 'blockquote') + { + $elements.append('<br />'); + } + + $elements.contents().unwrap(); + + if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove(); + + $.each(this.block.blocks, $.proxy(this.utils.removeEmpty, this)); + + $formatted.append(this.selection.getMarker(2)); + + if (!this.opts.linebreaks) + { + this.block.toggle($formatted); + } + + this.$editor.find('ul, ol, tr, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); + $formatted.find('blockquote:empty').remove(); + + if (this.block.isRemoveInline) + { + this.utils.removeInlineTags($formatted); + } + + if (this.opts.linebreaks && tag == 'p') + { + this.utils.replaceWithContents($formatted); + } + + }, + formatTableWrapping: function($formatted) + { + if ($formatted.closest('table').size() === 0) return; + + if ($formatted.closest('tr').size() === 0) $formatted.wrap('<tr>'); + if ($formatted.closest('td').size() === 0 && $formatted.closest('th').size() === 0) + { + $formatted.wrap('<td>'); + } + }, + removeData: function(name, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).removeAttr('data-' + name); + + this.code.sync(); + }, + setData: function(name, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).attr('data-' + name, value); + + this.code.sync(); + }, + toggleData: function(name, value) + { + var blocks = this.selection.getBlocks(); + $.each(blocks, function() + { + if ($(this).attr('data-' + name)) + { + $(this).removeAttr('data-' + name); + } + else + { + $(this).attr('data-' + name, value); + } + }); + }, + removeAttr: function(attr, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).removeAttr(attr); + + this.code.sync(); + }, + setAttr: function(attr, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).attr(attr, value); + + this.code.sync(); + }, + toggleAttr: function(attr, value) + { + var blocks = this.selection.getBlocks(); + $.each(blocks, function() + { + if ($(this).attr(name)) + { + $(this).removeAttr(name); + } + else + { + $(this).attr(name, value); + } + }); + }, + removeClass: function(className) + { + var blocks = this.selection.getBlocks(); + $(blocks).removeClass(className); + + this.utils.removeEmptyAttr(blocks, 'class'); + + this.code.sync(); + }, + setClass: function(className) + { + var blocks = this.selection.getBlocks(); + $(blocks).addClass(className); + + this.code.sync(); + }, + toggleClass: function(className) + { + var blocks = this.selection.getBlocks(); + $(blocks).toggleClass(className); + + this.code.sync(); } }; }, + buffer: function() + { + return { + set: function(type) + { + if (typeof type == 'undefined' || type == 'undo') + { + this.buffer.setUndo(); + } + else + { + this.buffer.setRedo(); + } + }, + setUndo: function() + { + this.selection.save(); + this.opts.buffer.push(this.$editor.html()); + this.selection.restore(); + }, + setRedo: function() + { + this.selection.save(); + this.opts.rebuffer.push(this.$editor.html()); + this.selection.restore(); + }, + getUndo: function() + { + this.$editor.html(this.opts.buffer.pop()); + }, + getRedo: function() + { + this.$editor.html(this.opts.rebuffer.pop()); + }, + add: function() + { + this.opts.buffer.push(this.$editor.html()); + }, + undo: function() + { + if (this.opts.buffer.length === 0) return; + + this.buffer.set('redo'); + this.buffer.getUndo(); + + this.selection.restore(); + + setTimeout($.proxy(this.observe.load, this), 50); + }, + redo: function() + { + if (this.opts.rebuffer.length === 0) return; + + this.buffer.set('undo'); + this.buffer.getRedo(); + + this.selection.restore(); + + setTimeout($.proxy(this.observe.load, this), 50); + } + }; + }, build: function() { return { run: function() { @@ -639,15 +1243,22 @@ { e = e.originalEvent || e; if (window.FormData === undefined || !e.dataTransfer) return true; - var length = e.dataTransfer.files.length; - if (length === 0) return true; + var length = e.dataTransfer.files.length; + if (length === 0) + { + this.code.sync(); + setTimeout($.proxy(this.clean.clearUnverified, this), 1); + this.core.setCallback('drop', e); + + return true; + } else { - e.preventDefault(); + e.preventDefault(); if (this.opts.dragImageUpload || this.opts.dragFileUpload) { var files = e.dataTransfer.files; this.upload.directUpload(files[0], e); @@ -739,28 +1350,34 @@ if (!this.opts.plugins) return; if (!RedactorPlugins) return; $.each(this.opts.plugins, $.proxy(function(i, s) { - if (RedactorPlugins[s]) + if (typeof RedactorPlugins[s] === 'undefined') return; + + if ($.inArray(s, $.Redactor.modules) !== -1) { - if (!$.isFunction(RedactorPlugins[s])) return; + $.error('Plugin name "' + s + '" matches the name of the Redactor\'s module.'); + return; + } - this[s] = RedactorPlugins[s](); + if (!$.isFunction(RedactorPlugins[s])) return; - var methods = this.getModuleMethods(this[s]); - var len = methods.length; + this[s] = RedactorPlugins[s](); - // bind methods - for (var z = 0; z < len; z++) - { - this[s][methods[z]] = this[s][methods[z]].bind(this); - } + var methods = this.getModuleMethods(this[s]); + var len = methods.length; - if ($.isFunction(this[s].init)) this[s].init(); + // bind methods + for (var z = 0; z < len; z++) + { + this[s][methods[z]] = this[s][methods[z]].bind(this); } + if ($.isFunction(this[s].init)) this[s].init(); + + }, this)); }, disableMozillaEditing: function() @@ -773,373 +1390,16 @@ document.execCommand('enableInlineTableEditing', false, false); } catch (e) {} } }; }, - lang: function() - { - return { - load: function() - { - this.opts.curLang = this.opts.langs[this.opts.lang]; - }, - get: function(name) - { - return (typeof this.opts.curLang[name] != 'undefined') ? this.opts.curLang[name] : ''; - } - }; - }, - toolbar: function() - { - return { - init: function() - { - return { - html: - { - title: this.lang.get('html'), - func: 'code.toggle' - }, - formatting: - { - title: this.lang.get('formatting'), - dropdown: - { - p: - { - title: this.lang.get('paragraph'), - func: 'block.format' - }, - blockquote: - { - title: this.lang.get('quote'), - func: 'block.format' - }, - pre: - { - title: this.lang.get('code'), - func: 'block.format' - }, - h1: - { - title: this.lang.get('header1'), - func: 'block.format' - }, - h2: - { - title: this.lang.get('header2'), - func: 'block.format' - }, - h3: - { - title: this.lang.get('header3'), - func: 'block.format' - }, - h4: - { - title: this.lang.get('header4'), - func: 'block.format' - }, - h5: - { - title: this.lang.get('header5'), - func: 'block.format' - } - } - }, - bold: - { - title: this.lang.get('bold'), - func: 'inline.format' - }, - italic: - { - title: this.lang.get('italic'), - func: 'inline.format' - }, - deleted: - { - title: this.lang.get('deleted'), - func: 'inline.format' - }, - underline: - { - title: this.lang.get('underline'), - func: 'inline.format' - }, - unorderedlist: - { - title: '&bull; ' + this.lang.get('unorderedlist'), - func: 'list.toggle' - }, - orderedlist: - { - title: '1. ' + this.lang.get('orderedlist'), - func: 'list.toggle' - }, - outdent: - { - title: '< ' + this.lang.get('outdent'), - func: 'indent.decrease' - }, - indent: - { - title: '> ' + this.lang.get('indent'), - func: 'indent.increase' - }, - image: - { - title: this.lang.get('image'), - func: 'image.show' - }, - file: - { - title: this.lang.get('file'), - func: 'file.show' - }, - link: - { - title: this.lang.get('link'), - dropdown: - { - link: - { - title: this.lang.get('link_insert'), - func: 'link.show' - }, - unlink: - { - title: this.lang.get('unlink'), - func: 'link.unlink' - } - } - }, - alignment: - { - title: this.lang.get('alignment'), - dropdown: - { - left: - { - title: this.lang.get('align_left'), - func: 'alignment.left' - }, - center: - { - title: this.lang.get('align_center'), - func: 'alignment.center' - }, - right: - { - title: this.lang.get('align_right'), - func: 'alignment.right' - }, - justify: - { - title: this.lang.get('align_justify'), - func: 'alignment.justify' - } - } - }, - horizontalrule: - { - title: this.lang.get('horizontalrule'), - func: 'line.insert' - } - }; - }, - build: function() - { - this.toolbar.hideButtons(); - this.toolbar.hideButtonsOnMobile(); - this.toolbar.isButtonSourceNeeded(); - - if (this.opts.buttons.length === 0) return; - - this.$toolbar = this.toolbar.createContainer(); - - this.toolbar.setOverflow(); - this.toolbar.append(); - this.toolbar.setFormattingTags(); - this.toolbar.loadButtons(); - this.toolbar.setTabindex(); - this.toolbar.setFixed(); - - // buttons response - if (this.opts.activeButtons) - { - this.$editor.on('mouseup.redactor keyup.redactor focus.redactor', $.proxy(this.observe.buttons, this)); - } - - }, - createContainer: function() - { - return $('<ul>').addClass('redactor-toolbar').attr('id', 'redactor-toolbar-' + this.uuid); - }, - setFormattingTags: function() - { - $.each(this.opts.toolbar.formatting.dropdown, $.proxy(function (i, s) - { - if ($.inArray(i, this.opts.formatting) == -1) delete this.opts.toolbar.formatting.dropdown[i]; - }, this)); - - }, - loadButtons: function() - { - $.each(this.opts.buttons, $.proxy(function(i, btnName) - { - if (!this.opts.toolbar[btnName]) return; - - if (this.opts.fileUpload === false && btnName === 'file') return true; - if ((this.opts.imageUpload === false && this.opts.s3 === false) && btnName === 'image') return true; - - var btnObject = this.opts.toolbar[btnName]; - this.$toolbar.append($('<li>').append(this.button.build(btnName, btnObject))); - - }, this)); - }, - append: function() - { - if (this.opts.toolbarExternal) - { - this.$toolbar.addClass('redactor-toolbar-external'); - $(this.opts.toolbarExternal).html(this.$toolbar); - } - else - { - this.$box.prepend(this.$toolbar); - } - }, - setFixed: function() - { - if (this.utils.isMobile()) return; - if (this.opts.toolbarExternal) return; - if (!this.opts.toolbarFixed) return; - - this.toolbar.observeScroll(); - $(this.opts.toolbarFixedTarget).on('scroll.redactor', $.proxy(this.toolbar.observeScroll, this)); - - }, - setTabindex: function() - { - this.$toolbar.find('a').attr('tabindex', '-1'); - }, - setOverflow: function() - { - if (this.utils.isMobile() && this.opts.toolbarOverflow) - { - this.$toolbar.addClass('redactor-toolbar-overflow'); - } - }, - isButtonSourceNeeded: function() - { - if (this.opts.buttonSource) return; - - var index = this.opts.buttons.indexOf('html'); - if (index !== -1) - { - this.opts.buttons.splice(index, 1); - } - }, - hideButtons: function() - { - if (this.opts.buttonsHide.length === 0) return; - - $.each(this.opts.buttonsHide, $.proxy(function(i, s) - { - var index = this.opts.buttons.indexOf(s); - this.opts.buttons.splice(index, 1); - - }, this)); - }, - hideButtonsOnMobile: function() - { - if (!this.utils.isMobile() && this.opts.buttonsHideOnMobile.length === 0) return; - - $.each(this.opts.buttonsHideOnMobile, $.proxy(function(i, s) - { - var index = this.opts.buttons.indexOf(s); - this.opts.buttons.splice(index, 1); - - }, this)); - }, - observeScroll: function() - { - var scrollTop = $(this.opts.toolbarFixedTarget).scrollTop(); - var boxTop = 1; - - if (this.opts.toolbarFixedTarget === document) - { - boxTop = this.$box.offset().top; - } - - if (scrollTop > boxTop) - { - this.toolbar.observeScrollEnable(scrollTop, boxTop); - } - else - { - this.toolbar.observeScrollDisable(); - } - }, - observeScrollEnable: function(scrollTop, boxTop) - { - var top = this.opts.toolbarFixedTopOffset + scrollTop - boxTop; - var left = 0; - var end = boxTop + this.$box.height() + 30; - var width = this.$box.innerWidth(); - - this.$toolbar.addClass('toolbar-fixed-box'); - this.$toolbar.css({ - position: 'absolute', - width: width, - top: top + 'px', - left: left - }); - - this.toolbar.setDropdownsFixed(); - this.$toolbar.css('visibility', (scrollTop < end) ? 'visible' : 'hidden'); - }, - observeScrollDisable: function() - { - this.$toolbar.css({ - position: 'relative', - width: 'auto', - top: 0, - left: 0, - visibility: 'visible' - }); - - this.toolbar.unsetDropdownsFixed(); - this.$toolbar.removeClass('toolbar-fixed-box'); - - }, - setDropdownsFixed: function() - { - var self = this; - $('.redactor-dropdown').each(function() - { - $(this).css({ position: 'fixed', top: self.$toolbar.innerHeight() + self.opts.toolbarFixedTopOffset }); - }); - }, - unsetDropdownsFixed: function() - { - var self = this; - $('.redactor-dropdown').each(function() - { - var top = (self.$toolbar.innerHeight() + self.$toolbar.offset().top) + 'px'; - $(this).css({ position: 'absolute', top: top }); - }); - } - }; - }, button: function() { return { build: function(btnName, btnObject) { - var $button = $('<a href="#" class="re-icon re-' + btnName + '" rel="' + btnName + '" />'); + var $button = $('<a href="#" class="re-icon re-' + btnName + '" rel="' + btnName + '" />').attr('tabindex', '-1'); if (btnObject.func || btnObject.command || btnObject.dropdown) { $button.on('touchstart click', $.proxy(function(e) { @@ -1205,10 +1465,12 @@ }); }, onClick: function(e, btnName, type, callback) { + this.button.caretOffset = this.caret.getOffset(); + e.preventDefault(); if (this.utils.browser('msie')) e.returnValue = false; if (type == 'command') @@ -1220,10 +1482,11 @@ this.dropdown.show(e, btnName); } else { var func; + if ($.isFunction(callback)) { callback.call(this, btnName); this.observe.buttons(e, btnName); } @@ -1240,11 +1503,10 @@ { this[callback](btnName); this.observe.buttons(e, btnName); } } - }, get: function(key) { return this.$toolbar.find('a.re-' + key); }, @@ -1363,303 +1625,213 @@ { this.button.get(key).remove(); } }; }, - dropdown: function() + caret: function() { return { - build: function(name, $dropdown, dropdownObject) + setStart: function(node) { - if (name == 'formatting' && this.opts.formattingAdd) + // inline tag + if (!this.utils.isBlock(node)) { - $.each(this.opts.formattingAdd, $.proxy(function(i,s) - { - var name = s.tag; - if (typeof s.class != 'undefined') - { - name = name + '-' + s.class; - } + var space = this.utils.createSpaceElement(); - s.type = (this.utils.isBlockTag(s.tag)) ? 'block' : 'inline'; - var func = (s.type == 'inline') ? 'inline.formatting' : 'block.formatting'; - - if (this.opts.linebreaks && s.type == 'block' && s.tag == 'p') return; - - this.formatting[name] = { - tag: s.tag, - style: s.style, - 'class': s.class, - attr: s.attr, - data: s.data - }; - - dropdownObject[name] = { - func: func, - title: s.title - }; - - }, this)); - + $(node).prepend(space); + this.caret.setEnd(space); } - - $.each(dropdownObject, $.proxy(function(btnName, btnObject) + else { - var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '">' + btnObject.title + '</a>'); - if (name == 'formatting') $item.addClass('redactor-formatting-' + btnName); - - $item.on('click', $.proxy(function(e) - { - var type = 'func'; - var callback = btnObject.func; - if (btnObject.command) - { - type = 'command'; - callback = btnObject.command; - } - else if (btnObject.dropdown) - { - type = 'dropdown'; - callback = btnObject.dropdown; - } - - this.button.onClick(e, btnName, type, callback); - - }, this)); - - $dropdown.append($item); - - }, this)); + this.caret.set(node, 0, node, 0); + } }, - show: function(e, key) + setEnd: function(node) { - if (!this.opts.visual) - { - e.preventDefault(); - return false; - } + this.caret.set(node, 1, node, 1); + }, + set: function(orgn, orgo, focn, foco) + { + // focus + if (!this.utils.browser('msie')) this.$editor.focus(); - var $button = this.button.get(key); + orgn = orgn[0] || orgn; + focn = focn[0] || focn; - // Always re-append it to the end of <body> so it always has the highest sub-z-index. - var $dropdown = $button.data('dropdown').appendTo(document.body); - - if ($button.hasClass('dropact')) + if (this.utils.isBlockTag(orgn.tagName) && orgn.innerHTML === '') { - this.dropdown.hideAll(); + orgn.innerHTML = this.opts.invisibleSpace; } - else + + if (orgn.tagName == 'BR' && this.opts.linebreaks === false) { - this.dropdown.hideAll(); - this.core.setCallback('dropdownShow', { dropdown: $dropdown, key: key, button: $button }); + var par = $(this.opts.emptyHtml)[0]; + $(orgn).replaceWith(par); + orgn = par; + focn = orgn; + } - this.button.setActive(key); + this.selection.get(); - $button.addClass('dropact'); + try { + this.range.setStart(orgn, orgo); + this.range.setEnd(focn, foco); + } + catch (e) {} - var keyPosition = $button.offset(); + this.selection.addRange(); + }, + setAfter: function(node) + { + try { + var tag = $(node)[0].tagName; - // fix right placement - var dropdownWidth = $dropdown.width(); - if ((keyPosition.left + dropdownWidth) > $(document).width()) + // inline tag + if (tag != 'BR' && !this.utils.isBlock(node)) { - keyPosition.left -= dropdownWidth; - } + var space = this.utils.createSpaceElement(); - var left = keyPosition.left + 'px'; - if (this.$toolbar.hasClass('toolbar-fixed-box')) - { - $dropdown.css({ position: 'fixed', left: left, top: this.$toolbar.innerHeight() + this.opts.toolbarFixedTopOffset }).show(); + $(node).after(space); + this.caret.setEnd(space); } else { - var top = ($button.innerHeight() + keyPosition.top) + 'px'; - - $dropdown.css({ position: 'absolute', left: left, top: top }).show(); + if (tag != 'BR' && this.utils.browser('msie')) + { + this.caret.setStart($(node).next()); + } + else + { + this.caret.setAfterOrBefore(node, 'after'); + } } - - - this.core.setCallback('dropdownShown', { dropdown: $dropdown, key: key, button: $button }); } - - $(document).one('click', $.proxy(this.dropdown.hide, this)); - this.$editor.one('click', $.proxy(this.dropdown.hide, this)); - - $dropdown.on('mouseover', function() { $('html').css('overflow', 'hidden'); }); - $dropdown.on('mouseout', function() { $('html').css('overflow', ''); }); - - e.stopPropagation(); + catch (e) { + var space = this.utils.createSpaceElement(); + $(node).after(space); + this.caret.setEnd(space); + } }, - hideAll: function() + setBefore: function(node) { - this.$toolbar.find('a.dropact').removeClass('redactor-act').removeClass('dropact'); - - $('.redactor-dropdown').hide(); - this.core.setCallback('dropdownHide'); - }, - hide: function (e) - { - var $dropdown = $(e.target); - if (!$dropdown.hasClass('dropact')) + // block tag + if (this.utils.isBlock(node)) { - $dropdown.removeClass('dropact'); - this.dropdown.hideAll(); + this.caret.setEnd($(node).prev()); } - } - }; - }, - code: function() - { - return { - set: function(html) - { - html = $.trim(html.toString()); - - // clean - html = this.clean.onSet(html); - - this.$editor.html(html); - this.code.sync(); - - setTimeout($.proxy(this.buffer.add, this), 15); - if (this.start === false) this.observe.load(); - + else + { + this.caret.setAfterOrBefore(node, 'before'); + } }, - get: function() + setAfterOrBefore: function(node, type) { - var code = this.$textarea.val(); + // focus + if (!this.utils.browser('msie')) this.$editor.focus(); - // indent code - code = this.tabifier.get(code); + node = node[0] || node; - return code; - }, - sync: function() - { - setTimeout($.proxy(this.code.startSync, this), 10); - }, - startSync: function() - { - var html = this.$editor.html(); + this.selection.get(); - // is there a need to synchronize - if (this.code.syncCode && this.code.syncCode == html) + if (type == 'after') { - // do not sync - return; - } + try { - // save code - this.code.syncCode = html; - - // before clean callback - html = this.core.setCallback('syncBefore', html); - - // clean - html = this.clean.onSync(html); - - // set code - this.$textarea.val(html); - - // after sync callback - this.core.setCallback('sync', html); - - if (this.start === false) + this.range.setStartAfter(node); + this.range.setEndAfter(node); + } + catch (e) {} + } + else { - this.core.setCallback('change', html); + try { + this.range.setStartBefore(node); + this.range.setEndBefore(node); + } + catch (e) {} } - this.start = false; - // autosave on change - this.autosave.onChange(); + this.range.collapse(false); + this.selection.addRange(); }, - toggle: function() + getOffsetOfElement: function(node) { - if (this.opts.visual) - { - this.code.showCode(); - } - else - { - this.code.showVisual(); - } - }, - showCode: function() - { - this.code.offset = this.caret.getOffset(); - var scroll = $(window).scrollTop(); + node = node[0] || node; - var height = this.$editor.innerHeight(); + this.selection.get(); - this.$editor.hide(); + var cloned = this.range.cloneRange(); + cloned.selectNodeContents(node); + cloned.setEnd(this.range.endContainer, this.range.endOffset); - var html = this.$textarea.val(); - this.modified = this.clean.removeSpaces(html); + return $.trim(cloned.toString()).length; + }, + getOffset: function() + { + var offset = 0; + var sel = window.getSelection(); - // indent code - html = this.tabifier.get(html); + if (sel.rangeCount > 0) + { + var range = window.getSelection().getRangeAt(0); + var caretRange = range.cloneRange(); + caretRange.selectNodeContents(this.$editor[0]); + caretRange.setEnd(range.endContainer, range.endOffset); + offset = caretRange.toString().length; + } - this.$textarea.val(html).height(height).show().focus(); - this.$textarea.on('keydown.redactor-textarea-indenting', this.code.textareaIndenting); - - $(window).scrollTop(scroll); - - this.opts.visual = false; - - this.button.setInactiveInCode(); - this.button.setActive('html'); - this.core.setCallback('source', html); + return offset; }, - showVisual: function() + setOffset: function(start, end) { - if (this.opts.visual) return; + if (typeof end == 'undefined') end = start; + if (!this.focus.isFocused()) this.focus.setStart(); - var html = this.$textarea.hide().val(); + var sel = this.selection.get(); + var node, offset = 0; + var walker = document.createTreeWalker(this.$editor[0], NodeFilter.SHOW_TEXT, null, null); - if (this.modified !== this.clean.removeSpaces(html)) + while (node = walker.nextNode()) { - this.code.set(html); - } + offset += node.nodeValue.length; + if (offset > start) + { + this.range.setStart(node, node.nodeValue.length + start - offset); + start = Infinity; + } - this.$editor.show(); - - if (!this.utils.isEmpty(html)) - { - this.placeholder.remove(); + if (offset >= end) + { + this.range.setEnd(node, node.nodeValue.length + end - offset); + break; + } } - this.caret.setOffset(this.code.offset); - - this.$textarea.off('keydown.redactor-textarea-indenting'); - - this.button.setActiveInVisual(); - this.button.setInactive('html'); - - this.observe.load(); - this.opts.visual = true; + this.range.collapse(false); + this.selection.addRange(); }, - textareaIndenting: function(e) + setToPoint: function(start, end) { - if (e.keyCode !== 9) return true; - - var $el = this.$textarea; - var start = $el.get(0).selectionStart; - $el.val($el.val().substring(0, start) + "\t" + $el.val().substring($el.get(0).selectionEnd)); - $el.get(0).selectionStart = $el.get(0).selectionEnd = start + 1; - - return false; + this.caret.setOffset(start, end); + }, + getCoords: function() + { + return this.caret.getOffset(); } }; }, clean: function() { return { onSet: function(html) { html = this.clean.savePreCode(html); + // convert script tag + html = html.replace(/<script(.*?[^>]?)>([\w\W]*?)<\/script>/gi, '<pre class="redactor-script-tag" style="display: none;" $1>$2</pre>'); + // replace dollar sign to entity html = html.replace(/\$/g, '&#36;'); html = html.replace(/”/g, '"'); html = html.replace(/‘/g, '\''); html = html.replace(/’/g, '\''); @@ -1669,12 +1841,26 @@ // save form tag html = this.clean.saveFormTags(html); // convert font tag to span - html = html.replace(/<font(.*?)style="(.*?)"(.*?)>([\w\W]*?)<\/font>/gi, '<span style="$2">$4</span>'); + var $div = $('<div>'); + $div.html(html); + var fonts = $div.find('font[style]'); + if (fonts.length !== 0) + { + fonts.replaceWith(function() + { + var $el = $(this); + var $span = $('<span>').attr('style', $el.attr('style')); + return $span.append($el.contents()); + }); + html = $div.html(); + } + $div.remove(); + // remove font tag html = html.replace(/<font(.*?[^<])>/gi, ''); html = html.replace(/<\/font>/gi, ''); // tidy html @@ -1694,17 +1880,24 @@ onSync: function(html) { // remove spaces html = html.replace(/[\u200B-\u200D\uFEFF]/g, ''); html = html.replace(/&#x200b;/gi, ''); - html = html.replace(/&nbsp;/gi, ' '); - if (html.search(/^<p>(||\s||&nbsp;)<\/p>$/i) != -1) + if (this.opts.cleanSpaces) { + html = html.replace(/&nbsp;/gi, ' '); + } + + if (html.search(/^<p>(||\s||<br\s?\/?>||&nbsp;)<\/p>$/i) != -1) + { return ''; } + // reconvert script tag + html = html.replace(/<pre class="redactor-script-tag" style="display: none;"(.*?[^>]?)>([\w\W]*?)<\/pre>/gi, '<script$1>$2</script>'); + // restore form tag html = this.clean.restoreFormTags(html); var chars = { '\u2122': '&trade;', @@ -1720,24 +1913,29 @@ }); // remove br in the of li html = html.replace(new RegExp('<br\\s?/?></li>', 'gi'), '</li>'); html = html.replace(new RegExp('</li><br\\s?/?>', 'gi'), '</li>'); - // remove verified - html = html.replace(new RegExp('<div(.*?) data-tagblock="redactor"(.*?[^>])>', 'gi'), '<div$1$2>'); + html = html.replace(new RegExp('<div(.*?[^>]) data-tagblock="redactor"(.*?[^>])>', 'gi'), '<div$1$2>'); html = html.replace(new RegExp('<(.*?) data-verified="redactor"(.*?[^>])>', 'gi'), '<$1$2>'); - html = html.replace(new RegExp('<span(.*?) rel="(.*?)"(.*?[^>])>', 'gi'), '<span$1$3>'); - html = html.replace(new RegExp('<img(.*?) rel="(.*?)"(.*?[^>])>', 'gi'), '<img$1$3>'); + html = html.replace(new RegExp('<span(.*?[^>])\srel="(.*?[^>])"(.*?[^>])>', 'gi'), '<span$1$3>'); + html = html.replace(new RegExp('<img(.*?[^>])\srel="(.*?[^>])"(.*?[^>])>', 'gi'), '<img$1$3>'); + html = html.replace(new RegExp('<img(.*?[^>])\sstyle="" (.*?[^>])>', 'gi'), '<img$1 $2>'); + html = html.replace(new RegExp('<img(.*?[^>])\sstyle (.*?[^>])>', 'gi'), '<img$1 $2>'); html = html.replace(new RegExp('<span class="redactor-invisible-space">(.*?)</span>', 'gi'), '$1'); html = html.replace(/ data-save-url="(.*?[^>])"/gi, ''); // remove image resize html = html.replace(/<span(.*?)id="redactor-image-box"(.*?[^>])>([\w\W]*?)<img(.*?)><\/span>/gi, '$3<img$4>'); html = html.replace(/<span(.*?)id="redactor-image-resizer"(.*?[^>])>(.*?)<\/span>/gi, ''); html = html.replace(/<span(.*?)id="redactor-image-editter"(.*?[^>])>(.*?)<\/span>/gi, ''); + // remove font tag + html = html.replace(/<font(.*?[^<])>/gi, ''); + html = html.replace(/<\/font>/gi, ''); + // tidy html html = this.tidy.load(html); // link nofollow if (this.opts.linkNofollow) @@ -1830,16 +2028,17 @@ if (this.opts.replaceDivs) html = this.clean.replaceDivs(html); html = this.clean.saveFormTags(html); } - html = this.clean.onPasteIeFixLinks(html); + html = this.clean.onPasteWord(html); html = this.clean.onPasteExtra(html); html = this.clean.onPasteTidy(html, 'all'); + // paragraphize if (!this.clean.singleLine && this.opts.paragraphize) { html = this.paragraphize.load(html); } @@ -1858,43 +2057,48 @@ html = html.replace(/<!--[\s\S]*?-->/gi, ''); // style html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ''); - // shapes - html = html.replace(/<img(.*?)v:shapes=(.*?)>/gi, ''); - html = html.replace(/src="file\:\/\/(.*?)"/, 'src=""'); + if (/(class=\"?Mso|style=\"[^\"]*\bmso\-|w:WordDocument)/.test(html)) + { + html = this.clean.onPasteIeFixLinks(html); - // list - html = html.replace(/<p(.*?)class="MsoListParagraphCxSpFirst"([\w\W]*?)<\/p>/gi, '<ul><li$2</li>'); - html = html.replace(/<p(.*?)class="MsoListParagraphCxSpMiddle"([\w\W]*?)<\/p>/gi, '<li$2</li>'); - html = html.replace(/<p(.*?)class="MsoListParagraphCxSpLast"([\w\W]*?)<\/p>/gi, '<li$2</li></ul>'); - // one line - html = html.replace(/<p(.*?)class="MsoListParagraph"([\w\W]*?)<\/p>/gi, '<ul><li$2</li></ul>'); - // remove ms word's bullet - html = html.replace(/·/g, ''); - html = html.replace(/<p class="Mso(.*?)"/gi, '<p'); + // shapes + html = html.replace(/<img(.*?)v:shapes=(.*?)>/gi, ''); + html = html.replace(/src="file\:\/\/(.*?)"/, 'src=""'); - // classes - html = html.replace(/ class=\"(mso[^\"]*)\"/gi, ""); - html = html.replace(/ class=(mso\w+)/gi, ""); + // list + html = html.replace(/<p(.*?)class="MsoListParagraphCxSpFirst"([\w\W]*?)<\/p>/gi, '<ul><li$2</li>'); + html = html.replace(/<p(.*?)class="MsoListParagraphCxSpMiddle"([\w\W]*?)<\/p>/gi, '<li$2</li>'); + html = html.replace(/<p(.*?)class="MsoListParagraphCxSpLast"([\w\W]*?)<\/p>/gi, '<li$2</li></ul>'); + // one line + html = html.replace(/<p(.*?)class="MsoListParagraph"([\w\W]*?)<\/p>/gi, '<ul><li$2</li></ul>'); + // remove ms word's bullet + html = html.replace(/·/g, ''); + html = html.replace(/<p class="Mso(.*?)"/gi, '<p'); - // remove ms word tags - html = html.replace(/<o:p(.*?)>([\w\W]*?)<\/o:p>/gi, '$2'); + // classes + html = html.replace(/ class=\"(mso[^\"]*)\"/gi, ""); + html = html.replace(/ class=(mso\w+)/gi, ""); + // remove ms word tags + html = html.replace(/<o:p(.*?)>([\w\W]*?)<\/o:p>/gi, '$2'); + + // ms word break lines + html = html.replace(/\n/g, ' '); + + // ms word lists break lines + html = html.replace(/<p>\n?<li>/gi, '<li>'); + } + // remove nbsp if (this.opts.cleanSpaces) { html = html.replace(/(\s|&nbsp;)+/g, ' '); } - // ms word break lines - html = html.replace(/\n/g, ' '); - - // ms word lists break lines - html = html.replace(/<p>\n?<li>/gi, '<li>'); - return html; }, onPasteExtra: function(html) { // remove google docs markers @@ -1936,17 +2140,18 @@ }, onPasteTidy: function(html, type) { // remove all tags except these var tags = ['span', 'a', 'pre', 'blockquote', 'small', 'em', 'strong', 'code', 'kbd', 'mark', 'address', 'cite', 'var', 'samp', 'dfn', 'sup', 'sub', 'b', 'i', 'u', 'del', - 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'p', 'br', 'video', 'audio', 'embed', 'param', 'object', 'img', 'table', + 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'p', 'br', 'video', 'audio', 'iframe', 'embed', 'param', 'object', 'img', 'table', 'td', 'th', 'tr', 'tbody', 'tfoot', 'thead', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; var tagsEmpty = false; var attrAllowed = [ ['a', '*'], ['img', ['src', 'alt']], ['span', ['class', 'rel', 'data-verified']], + ['iframe', '*'], ['video', '*'], ['audio', '*'], ['embed', '*'], ['object', '*'], ['param', '*'], @@ -1960,10 +2165,11 @@ ['table', 'class'], ['td', ['colspan', 'rowspan']], ['a', '*'], ['img', ['src', 'alt', 'data-redactor-inserted-image']], ['span', ['class', 'rel', 'data-verified']], + ['iframe', '*'], ['video', '*'], ['audio', '*'], ['embed', '*'], ['object', '*'], ['param', '*'], @@ -1972,18 +2178,18 @@ } else if (type == 'td') { // remove all tags except these and remove all table tags: tr, td etc tags = ['ul', 'ol', 'li', 'span', 'a', 'small', 'em', 'strong', 'code', 'kbd', 'mark', 'cite', 'var', 'samp', 'dfn', 'sup', 'sub', 'b', 'i', 'u', 'del', - 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'br', 'video', 'audio', 'embed', 'param', 'object', 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'br', 'iframe', 'video', 'audio', 'embed', 'param', 'object', 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; } else if (type == 'li') { // only inline tags and ul, ol, li tags = ['ul', 'ol', 'li', 'span', 'a', 'small', 'em', 'strong', 'code', 'kbd', 'mark', 'cite', 'var', 'samp', 'dfn', 'sup', 'sub', 'b', 'i', 'u', 'del', 'br', - 'video', 'audio', 'embed', 'param', 'object', 'img']; + 'iframe', 'video', 'audio', 'embed', 'param', 'object', 'img']; } var options = { deniedTags: false, allowedTags: tags, @@ -2129,11 +2335,11 @@ getOnlyImages: function(html) { html = html.replace(/<img(.*?)>/gi, '[img$1]'); // remove all tags - html = html.replace(/<(.*?)>/gi, ''); + html = html.replace(/<([Ss]*?)>/gi, ''); html = html.replace(/\[img(.*?)\]/gi, '<img$1>'); return html; }, @@ -2204,12 +2410,15 @@ if (matches) { var len = matches.length; for (var i = 0; i < len; i++) { - var newTag = matches[i].replace(/style="(.*?)"/i, 'style="$1" rel="$1"'); - html = html.replace(new RegExp(matches[i], 'gi'), newTag); + try { + var newTag = matches[i].replace(/style="(.*?)"/i, 'style="$1" rel="$1"'); + html = html.replace(new RegExp(matches[i], 'gi'), newTag); + } + catch (e) {} } } return html; }, @@ -2303,620 +2512,492 @@ { return html.replace(/<section(.*?) rel="redactor-form-tag"(.*?)>([\w\W]*?)<\/section>/gi, '<form$1$2>$3</form>'); } }; }, - tidy: function() + code: function() { return { - setupAllowed: function() + set: function(html) { - if (this.opts.allowedTags) this.opts.deniedTags = false; - if (this.opts.allowedAttr) this.opts.removeAttr = false; + html = $.trim(html.toString()); - if (this.opts.linebreaks) return; + // clean + html = this.clean.onSet(html); - var tags = ['p', 'section']; - if (this.opts.allowedTags) this.tidy.addToAllowed(tags); - if (this.opts.deniedTags) this.tidy.removeFromDenied(tags); + this.$editor.html(html); + this.code.sync(); + setTimeout($.proxy(this.buffer.add, this), 15); + if (this.start === false) this.observe.load(); + }, - addToAllowed: function(tags) + get: function() { - var len = tags.length; - for (var i = 0; i < len; i++) - { - if ($.inArray(tags[i], this.opts.allowedTags) == -1) - { - this.opts.allowedTags.push(tags[i]); - } - } + var code = this.$textarea.val(); + + // indent code + code = this.tabifier.get(code); + + return code; }, - removeFromDenied: function(tags) + sync: function() { - var len = tags.length; - for (var i = 0; i < len; i++) - { - var pos = $.inArray(tags[i], this.opts.deniedTags); - if (pos != -1) - { - this.opts.deniedTags.splice(pos, 1); - } - } + setTimeout($.proxy(this.code.startSync, this), 10); }, - load: function(html, options) + startSync: function() { - this.tidy.settings = { - deniedTags: this.opts.deniedTags, - allowedTags: this.opts.allowedTags, - removeComments: this.opts.removeComments, - replaceTags: this.opts.replaceTags, - replaceStyles: this.opts.replaceStyles, - removeDataAttr: this.opts.removeDataAttr, - removeAttr: this.opts.removeAttr, - allowedAttr: this.opts.allowedAttr, - removeWithoutAttr: this.opts.removeWithoutAttr, - removeEmpty: this.opts.removeEmpty - }; + var html = this.$editor.html(); - $.extend(this.tidy.settings, options); + // is there a need to synchronize + if (this.code.syncCode && this.code.syncCode == html) + { + // do not sync + return; + } - html = this.tidy.removeComments(html); - html = this.tidy.replaceTags(html); + // save code + this.code.syncCode = html; - // create container - this.tidy.$div = $('<div />').append(html); + // before clean callback + html = this.core.setCallback('syncBefore', html); // clean - this.tidy.replaceStyles(); - this.tidy.removeTags(); + html = this.clean.onSync(html); - this.tidy.removeAttr(); - this.tidy.removeEmpty(); - this.tidy.removeParagraphsInLists(); - this.tidy.removeDataAttr(); - this.tidy.removeWithoutAttr(); + // set code + this.$textarea.val(html); - html = this.tidy.$div.html(); - this.tidy.$div.remove(); + // after sync callback + this.core.setCallback('sync', html); - return html; - }, - removeComments: function(html) - { - if (!this.tidy.settings.removeComments) return html; - - return html.replace(/<!--[\s\S]*?-->/gi, ''); - }, - replaceTags: function(html) - { - if (!this.tidy.settings.replaceTags) return html; - - var len = this.tidy.settings.replaceTags.length; - for (var i = 0; i < len; i++) + if (this.start === false) { - var re = new RegExp('<' + this.tidy.settings.replaceTags[i][0] + '(.*?[^>])>', 'gi'); - html = html.replace(re, '<' + this.tidy.settings.replaceTags[i][1] + '$1>'); - - re = new RegExp('</' + this.tidy.settings.replaceTags[i][0] + '>', 'gi'); - html = html.replace(re, '</' + this.tidy.settings.replaceTags[i][1] + '>'); + this.core.setCallback('change', html); } + this.start = false; - return html; + // autosave on change + this.autosave.onChange(); }, - replaceStyles: function() + toggle: function() { - if (!this.tidy.settings.replaceStyles) return; - - var len = this.tidy.settings.replaceStyles.length; - this.tidy.$div.find('span').each($.proxy(function(n,s) + if (this.opts.visual) { - var $el = $(s); - var style = $el.attr('style'); - for (var i = 0; i < len; i++) - { - if (style && style.match(new RegExp('^' + this.tidy.settings.replaceStyles[i][0], 'i'))) - { - var tagName = this.tidy.settings.replaceStyles[i][1]; - $el.replaceWith(function() - { - var tag = document.createElement(tagName); - return $(tag).append($(this).contents()); - }); - } - } - - }, this)); - - }, - removeTags: function() - { - if (!this.tidy.settings.deniedTags && this.tidy.settings.allowedTags) - { - this.tidy.$div.find('*').not(this.tidy.settings.allowedTags.join(',')).contents().unwrap(); + this.code.showCode(); } - - if (this.tidy.settings.deniedTags) + else { - this.tidy.$div.find(this.tidy.settings.deniedTags.join(',')).contents().unwrap(); + this.code.showVisual(); } }, - removeAttr: function() + showCode: function() { - var len; - if (!this.tidy.settings.removeAttr && this.tidy.settings.allowedAttr) - { + this.code.offset = this.caret.getOffset(); + var scroll = $(window).scrollTop(); - var allowedAttrTags = [], allowedAttrData = []; - len = this.tidy.settings.allowedAttr.length; - for (var i = 0; i < len; i++) - { - allowedAttrTags.push(this.tidy.settings.allowedAttr[i][0]); - allowedAttrData.push(this.tidy.settings.allowedAttr[i][1]); - } + var height = this.$editor.innerHeight(); + this.$editor.hide(); - this.tidy.$div.find('*').each($.proxy(function(n,s) - { - var $el = $(s); - var pos = $.inArray($el[0].tagName.toLowerCase(), allowedAttrTags); - var attributesRemove = this.tidy.removeAttrGetRemoves(pos, allowedAttrData, $el); + var html = this.$textarea.val(); + this.modified = this.clean.removeSpaces(html); - if (attributesRemove) - { - $.each(attributesRemove, function(z,f) { - $el.removeAttr(f); - }); - } - }, this)); - } + // indent code + html = this.tabifier.get(html); - if (this.tidy.settings.removeAttr) - { - len = this.tidy.settings.removeAttr.length; - for (var i = 0; i < len; i++) - { - var attrs = this.tidy.settings.removeAttr[i][1]; - if ($.isArray(attrs)) attrs = attrs.join(' '); + this.$textarea.val(html).height(height).show().focus(); + this.$textarea.on('keydown.redactor-textarea-indenting', this.code.textareaIndenting); - this.tidy.$div.find(this.tidy.settings.removeAttr[i][0]).removeAttr(attrs); - } + $(window).scrollTop(scroll); + + if (this.$textarea[0].setSelectionRange) + { + this.$textarea[0].setSelectionRange(0, 0); } + this.$textarea[0].scrollTop = 0; + + this.opts.visual = false; + + this.button.setInactiveInCode(); + this.button.setActive('html'); + this.core.setCallback('source', html); }, - removeAttrGetRemoves: function(pos, allowed, $el) + showVisual: function() { - var attributesRemove = []; + if (this.opts.visual) return; - // remove all attrs - if (pos == -1) - { - $.each($el[0].attributes, function(i, item) - { - attributesRemove.push(item.name); - }); + var html = this.$textarea.hide().val(); - } - // allow all attrs - else if (allowed[pos] == '*') + if (this.modified !== this.clean.removeSpaces(html)) { - attributesRemove = []; + this.code.set(html); } - // allow specific attrs - else - { - $.each($el[0].attributes, function(i, item) - { - if ($.isArray(allowed[pos])) - { - if ($.inArray(item.name, allowed[pos]) == -1) - { - attributesRemove.push(item.name); - } - } - else if (allowed[pos] != item.name) - { - attributesRemove.push(item.name); - } - }); - } + this.$editor.show(); - return attributesRemove; - }, - removeAttrs: function (el, regex) - { - regex = new RegExp(regex, "g"); - return el.each(function() + if (!this.utils.isEmpty(html)) { - var self = $(this); - var len = this.attributes.length - 1; - for (var i = len; i >= 0; i--) - { - var item = this.attributes[i]; - if (item && item.specified && item.name.search(regex)>=0) - { - self.removeAttr(item.name); - } - } - }); - }, - removeEmpty: function() - { - if (!this.tidy.settings.removeEmpty) return; + this.placeholder.remove(); + } - this.tidy.$div.find(this.tidy.settings.removeEmpty.join(',')).each(function() - { - var $el = $(this); - var text = $el.text(); - text = text.replace(/[\u200B-\u200D\uFEFF]/g, ''); - text = text.replace(/&nbsp;/gi, ''); - text = text.replace(/\s/g, ''); + this.caret.setOffset(this.code.offset); - if (text === '' && $el.children().length === 0) - { - $el.remove(); - } - }); - }, - removeParagraphsInLists: function() - { - this.tidy.$div.find('li p').contents().unwrap(); - }, - removeDataAttr: function() - { - if (!this.tidy.settings.removeDataAttr) return; + this.$textarea.off('keydown.redactor-textarea-indenting'); - var tags = this.tidy.settings.removeDataAttr; - if ($.isArray(this.tidy.settings.removeDataAttr)) tags = this.tidy.settings.removeDataAttr.join(','); + this.button.setActiveInVisual(); + this.button.setInactive('html'); - this.tidy.removeAttrs(this.tidy.$div.find(tags), '^(data-)'); - + this.observe.load(); + this.opts.visual = true; }, - removeWithoutAttr: function() + textareaIndenting: function(e) { - if (!this.tidy.settings.removeWithoutAttr) return; + if (e.keyCode !== 9) return true; - this.tidy.$div.find(this.tidy.settings.removeWithoutAttr.join(',')).each(function() - { - if (this.attributes.length === 0) - { - $(this).contents().unwrap(); - } - }); + var $el = this.$textarea; + var start = $el.get(0).selectionStart; + $el.val($el.val().substring(0, start) + "\t" + $el.val().substring($el.get(0).selectionEnd)); + $el.get(0).selectionStart = $el.get(0).selectionEnd = start + 1; + + return false; } }; }, - paragraphize: function() + core: function() { return { - load: function(html) + getObject: function() { - if (this.opts.linebreaks) return html; - if (html === '' || html === '<p></p>') return this.opts.emptyHtml; - - this.paragraphize.blocks = ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption', - 'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea', - 'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p']; - - html = html + "\n"; - - this.paragraphize.safes = []; - this.paragraphize.z = 0; - - html = html.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>'); - - html = this.paragraphize.getSafes(html); - html = this.paragraphize.getSafesComments(html); - html = this.paragraphize.replaceBreaksToNewLines(html); - html = this.paragraphize.replaceBreaksToParagraphs(html); - html = this.paragraphize.clear(html); - html = this.paragraphize.restoreSafes(html); - - html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.paragraphize.blocks.join('|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>'); - - return $.trim(html); + return $.extend({}, this); }, - getSafes: function(html) + getEditor: function() { - var $div = $('<div />').append(html); - - // remove paragraphs in blockquotes - $div.find('blockquote p').replaceWith(function() - { - return $(this).append('<br />').contents(); - }); - - html = $div.html(); - - $div.find(this.paragraphize.blocks.join(', ')).each($.proxy(function(i,s) - { - this.paragraphize.z++; - this.paragraphize.safes[this.paragraphize.z] = s.outerHTML; - html = html.replace(s.outerHTML, '\n{replace' + this.paragraphize.z + '}'); - - }, this)); - - return html; + return this.$editor; }, - getSafesComments: function(html) + getBox: function() { - var commentsMatches = html.match(/<!--([\w\W]*?)-->/gi); - - if (!commentsMatches) return html; - - $.each(commentsMatches, $.proxy(function(i,s) - { - this.paragraphize.z++; - this.paragraphize.safes[this.paragraphize.z] = s; - html = html.replace(s, '\n{replace' + this.paragraphize.z + '}'); - }, this)); - - return html; + return this.$box; }, - restoreSafes: function(html) + getElement: function() { - $.each(this.paragraphize.safes, function(i,s) + return this.$element; + }, + getTextarea: function() + { + return this.$textarea; + }, + getToolbar: function() + { + return (this.$toolbar) ? this.$toolbar : false; + }, + addEvent: function(name) + { + this.core.event = name; + }, + getEvent: function() + { + return this.core.event; + }, + setCallback: function(type, e, data) + { + var callback = this.opts[type + 'Callback']; + if ($.isFunction(callback)) { - html = html.replace('{replace' + i + '}', s); - }); - - return html; + return (typeof data == 'undefined') ? callback.call(this, e) : callback.call(this, e, data); + } + else + { + return (typeof data == 'undefined') ? e : data; + } }, - replaceBreaksToParagraphs: function(html) + destroy: function() { - var htmls = html.split(new RegExp('\n', 'g'), -1); + this.core.setCallback('destroy'); - html = ''; - if (htmls) - { - var len = htmls.length; - for (var i = 0; i < len; i++) - { - if (!htmls.hasOwnProperty(i)) return; + // off events and remove data + this.$element.off('.redactor').removeData('redactor'); + this.$editor.off('.redactor'); - if (htmls[i].search('{replace') == -1) - { - htmls[i] = htmls[i].replace(/<p>\n\t?<\/p>/gi, ''); - htmls[i] = htmls[i].replace(/<p><\/p>/gi, ''); + // common + this.$editor.removeClass('redactor-editor redactor-linebreaks redactor-placeholder'); + this.$editor.removeAttr('contenteditable'); - if (htmls[i] !== '') - { - html += '<p>' + htmls[i].replace(/^\n+|\n+$/g, "") + "</p>"; - } - } - else html += htmls[i]; - } + var html = this.code.get(); + + if (this.build.isTextarea()) + { + this.$box.after(this.$element); + this.$box.remove(); + this.$element.val(html).show(); } + else + { + this.$box.after(this.$editor); + this.$box.remove(); + this.$element.html(html).show(); + } - return html; - }, - replaceBreaksToNewLines: function(html) - { - html = html.replace(/<br \/>\s*<br \/>/gi, "\n\n"); - html = html.replace(/<br\s?\/?>\n?<br\s?\/?>/gi, "\n<br /><br />"); + // paste box + if (this.$pasteBox) this.$pasteBox.remove(); - html = html.replace(new RegExp("\r\n", 'g'), "\n"); - html = html.replace(new RegExp("\r", 'g'), "\n"); - html = html.replace(new RegExp("/\n\n+/"), 'g', "\n\n"); + // modal + if (this.$modalBox) this.$modalBox.remove(); + if (this.$modalOverlay) this.$modalOverlay.remove(); - return html; - }, - clear: function(html) - { - html = html.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>'); - html = html.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>'); - html = html.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>'); - html = html.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>'); + // buttons tooltip + $('.redactor-toolbar-tooltip').remove(); - html = html.replace(new RegExp('<p><p ', 'gi'), '<p '); - html = html.replace(new RegExp('<p><p>', 'gi'), '<p>'); - html = html.replace(new RegExp('</p></p>', 'gi'), '</p>'); - html = html.replace(new RegExp('<p>\\s?</p>', 'gi'), ''); - html = html.replace(new RegExp("\n</p>", 'gi'), '</p>'); - html = html.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>'); - html = html.replace(new RegExp('<p>\t*</p>', 'gi'), ''); + // autosave + clearInterval(this.autosaveInterval); - return html; } }; }, - tabifier: function() + dropdown: function() { return { - get: function(code) + build: function(name, $dropdown, dropdownObject) { - if (!this.opts.tabifier) return code; + if (name == 'formatting' && this.opts.formattingAdd) + { + $.each(this.opts.formattingAdd, $.proxy(function(i,s) + { + var name = s.tag; + if (typeof s.class != 'undefined') + { + name = name + '-' + s.class; + } - // clean setup - var ownLine = ['area', 'body', 'head', 'hr', 'i?frame', 'link', 'meta', 'noscript', 'style', 'script', 'table', 'tbody', 'thead', 'tfoot']; - var contOwnLine = ['li', 'dt', 'dt', 'h[1-6]', 'option', 'script']; - var newLevel = ['blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'p', 'pre', 'select', 'td', 'th', 'tr', 'ul']; + s.type = (this.utils.isBlockTag(s.tag)) ? 'block' : 'inline'; + var func = (s.type == 'inline') ? 'inline.formatting' : 'block.formatting'; - this.tabifier.lineBefore = new RegExp('^<(/?' + ownLine.join('|/?' ) + '|' + contOwnLine.join('|') + ')[ >]'); - this.tabifier.lineAfter = new RegExp('^<(br|/?' + ownLine.join('|/?' ) + '|/' + contOwnLine.join('|/') + ')[ >]'); - this.tabifier.newLevel = new RegExp('^</?(' + newLevel.join('|' ) + ')[ >]'); + if (this.opts.linebreaks && s.type == 'block' && s.tag == 'p') return; - var i = 0, - codeLength = code.length, - point = 0, - start = null, - end = null, - tag = '', - out = '', - cont = ''; + this.formatting[name] = { + tag: s.tag, + style: s.style, + 'class': s.class, + attr: s.attr, + data: s.data, + clear: s.clear + }; - this.tabifier.cleanlevel = 0; + dropdownObject[name] = { + func: func, + title: s.title + }; - for (; i < codeLength; i++) + }, this)); + + } + + $.each(dropdownObject, $.proxy(function(btnName, btnObject) { - point = i; + var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '">' + btnObject.title + '</a>'); + if (name == 'formatting') $item.addClass('redactor-formatting-' + btnName); - // if no more tags, copy and exit - if (-1 == code.substr(i).indexOf( '<' )) + $item.on('click', $.proxy(function(e) { - out += code.substr(i); + var type = 'func'; + var callback = btnObject.func; + if (btnObject.command) + { + type = 'command'; + callback = btnObject.command; + } + else if (btnObject.dropdown) + { + type = 'dropdown'; + callback = btnObject.dropdown; + } - return this.tabifier.finish(out); - } + this.button.onClick(e, btnName, type, callback); - // copy verbatim until a tag - while (point < codeLength && code.charAt(point) != '<') - { - point++; - } + }, this)); - if (i != point) - { - cont = code.substr(i, point - i); - if (!cont.match(/^\s{2,}$/g)) - { - if ('\n' == out.charAt(out.length - 1)) out += this.tabifier.getTabs(); - else if ('\n' == cont.charAt(0)) - { - out += '\n' + this.tabifier.getTabs(); - cont = cont.replace(/^\s+/, ''); - } + $dropdown.append($item); - out += cont; - } + }, this)); + }, + show: function(e, key) + { + if (!this.opts.visual) + { + e.preventDefault(); + return false; + } - if (cont.match(/\n/)) out += '\n' + this.tabifier.getTabs(); - } + var $button = this.button.get(key); - start = point; + // Always re-append it to the end of <body> so it always has the highest sub-z-index. + var $dropdown = $button.data('dropdown').appendTo(document.body); - // find the end of the tag - while (point < codeLength && '>' != code.charAt(point)) - { - point++; - } + // ios keyboard hide + if (this.utils.isMobile() && !this.utils.browser('msie')) + { + document.activeElement.blur(); + } - tag = code.substr(start, point - start); - i = point; + if ($button.hasClass('dropact')) + { + this.dropdown.hideAll(); + } + else + { + this.dropdown.hideAll(); + this.core.setCallback('dropdownShow', { dropdown: $dropdown, key: key, button: $button }); - var t; + this.button.setActive(key); - if ('!--' == tag.substr(1, 3)) - { - if (!tag.match(/--$/)) - { - while ('-->' != code.substr(point, 3)) - { - point++; - } - point += 2; - tag = code.substr(start, point - start); - i = point; - } + $button.addClass('dropact'); - if ('\n' != out.charAt(out.length - 1)) out += '\n'; + var keyPosition = $button.offset(); - out += this.tabifier.getTabs(); - out += tag + '>\n'; - } - else if ('!' == tag[1]) + // fix right placement + var dropdownWidth = $dropdown.width(); + if ((keyPosition.left + dropdownWidth) > $(document).width()) { - out = this.tabifier.placeTag(tag + '>', out); + keyPosition.left -= dropdownWidth; } - else if ('?' == tag[1]) - { - out += tag + '>\n'; - } - else if (t = tag.match(/^<(script|style|pre)/i)) - { - t[1] = t[1].toLowerCase(); - tag = this.tabifier.cleanTag(tag); - out = this.tabifier.placeTag(tag, out); - end = String(code.substr(i + 1)).toLowerCase().indexOf('</' + t[1]); - if (end) + var left = keyPosition.left + 'px'; + if (this.$toolbar.hasClass('toolbar-fixed-box')) + { + var top = this.$toolbar.innerHeight() + this.opts.toolbarFixedTopOffset; + var position = 'fixed'; + if (this.opts.toolbarFixedTarget !== document) { - cont = code.substr(i + 1, end); - i += end; - out += cont; + top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top) + this.opts.toolbarFixedTopOffset; + position = 'absolute'; } + + $dropdown.css({ position: position, left: left, top: top + 'px' }).show(); } else { - tag = this.tabifier.cleanTag(tag); - out = this.tabifier.placeTag(tag, out); + var top = ($button.innerHeight() + keyPosition.top) + 'px'; + + $dropdown.css({ position: 'absolute', left: left, top: top }).show(); } + + + this.core.setCallback('dropdownShown', { dropdown: $dropdown, key: key, button: $button }); } - return this.tabifier.finish(out); + $(document).one('click', $.proxy(this.dropdown.hide, this)); + this.$editor.one('click', $.proxy(this.dropdown.hide, this)); + + // disable scroll whan dropdown scroll + var $body = $(document.body); + var width = $body.width(); + + $dropdown.on('mouseover', function() { + + $body.addClass('body-redactor-hidden'); + $body.css('margin-right', ($body.width() - width) + 'px'); + + }); + + $dropdown.on('mouseout', function() { + + $body.removeClass('body-redactor-hidden').css('margin-right', 0); + + }); + + + e.stopPropagation(); }, - getTabs: function() + hideAll: function() { - var s = ''; - for ( var j = 0; j < this.tabifier.cleanlevel; j++ ) - { - s += '\t'; - } + this.$toolbar.find('a.dropact').removeClass('redactor-act').removeClass('dropact'); - return s; + $(document.body).removeClass('body-redactor-hidden').css('margin-right', 0); + $('.redactor-dropdown').hide(); + this.core.setCallback('dropdownHide'); }, - finish: function(code) + hide: function (e) { - code = code.replace(/\n\s*\n/g, '\n'); - code = code.replace(/^[\s\n]*/, ''); - code = code.replace(/[\s\n]*$/, ''); - code = code.replace(/<script(.*?)>\n<\/script>/gi, '<script$1></script>'); + var $dropdown = $(e.target); + if (!$dropdown.hasClass('dropact')) + { + $dropdown.removeClass('dropact'); + this.dropdown.hideAll(); + } + } + }; + }, + file: function() + { + return { + show: function() + { + this.modal.load('file', this.lang.get('file'), 700); + this.upload.init('#redactor-modal-file-upload', this.opts.fileUpload, this.file.insert); - this.tabifier.cleanlevel = 0; + this.selection.save(); - return code; + this.selection.get(); + var text = this.sel.toString(); + + $('#redactor-filename').val(text); + + this.modal.show(); }, - cleanTag: function (tag) + insert: function(json, direct, e) { - var tagout = ''; - tag = tag.replace(/\n/g, ' '); - tag = tag.replace(/\s{2,}/g, ' '); - tag = tag.replace(/^\s+|\s+$/g, ' '); - - var suffix = ''; - if (tag.match(/\/$/)) + // error callback + if (typeof json.error != 'undefined') { - suffix = '/'; - tag = tag.replace(/\/+$/, ''); + this.modal.close(); + this.selection.restore(); + this.core.setCallback('fileUploadError', json); + return; } - var m; - while (m = /\s*([^= ]+)(?:=((['"']).*?\3|[^ ]+))?/.exec(tag)) + var link; + if (typeof json == 'string') { - if (m[2]) tagout += m[1].toLowerCase() + '=' + m[2]; - else if (m[1]) tagout += m[1].toLowerCase(); + link = json; + } + else + { + var text = $('#redactor-filename').val(); + if (typeof text == 'undefined' || text === '') text = json.filename; - tagout += ' '; - tag = tag.substr(m[0].length); + link = '<a href="' + json.filelink + '" id="filelink-marker">' + text + '</a>'; } - return tagout.replace(/\s*$/, '') + suffix + '>'; - }, - placeTag: function (tag, out) - { - var nl = tag.match(this.tabifier.newLevel); - if (tag.match(this.tabifier.lineBefore) || nl) + if (direct) { - out = out.replace(/\s*$/, ''); - out += '\n'; + this.selection.removeMarkers(); + var marker = this.selection.getMarker(); + this.insert.nodeToCaretPositionFromPoint(e, marker); } + else + { + this.modal.close(); + } - if (nl && '/' == tag.charAt(1)) this.tabifier.cleanlevel--; - if ('\n' == out.charAt(out.length - 1)) out += this.tabifier.getTabs(); - if (nl && '/' != tag.charAt(1)) this.tabifier.cleanlevel++; + this.selection.restore(); + this.buffer.set(); - out += tag; + this.insert.htmlWithoutClean(link); - if (tag.match(this.tabifier.lineAfter) || tag.match(this.tabifier.newLevel)) + if (typeof json == 'string') return; + + var linkmarker = $(this.$editor.find('a#filelink-marker')); + if (linkmarker.size() !== 0) { - out = out.replace(/ *$/, ''); - out += '\n'; + linkmarker.removeAttr('id').removeAttr('style'); } + else linkmarker = false; - return out; + this.core.setCallback('fileUpload', linkmarker, json); + } }; }, focus: function() { @@ -2935,11 +3016,11 @@ if (first[0].tagName == 'UL' || first[0].tagName == 'OL') { first = first.find('li').first(); var child = first.children().first(); - if (!this.utils.isBlock(child) && child.text() == '') + if (!this.utils.isBlock(child) && child.text() === '') { // empty inline tag in li this.caret.setStart(child); return; } @@ -2989,167 +3070,481 @@ return this.$editor.is(':focus'); } }; }, - placeholder: function() + image: function() { return { - enable: function() + show: function() { - if (!this.placeholder.is()) return; + this.modal.load('image', this.lang.get('image'), 700); + this.upload.init('#redactor-modal-image-droparea', this.opts.imageUpload, this.image.insert); - this.$editor.attr('placeholder', this.$element.attr('placeholder')); + this.selection.save(); + this.modal.show(); - this.placeholder.toggle(); - this.$editor.on('keyup.redactor-placeholder', $.proxy(this.placeholder.toggle, this)); - }, - toggle: function() + showEdit: function($image) { - var func = 'removeClass'; - if (this.utils.isEmpty(this.$editor.html(), false)) func = 'addClass'; - this.$editor[func]('redactor-placeholder'); - }, - remove: function() - { - this.$editor.removeClass('redactor-placeholder'); - }, - is: function() - { - if (this.opts.placeholder) + var $link = $image.closest('a'); + + this.modal.load('imageEdit', this.lang.get('edit'), 705); + + this.modal.createCancelButton(); + this.image.buttonDelete = this.modal.createDeleteButton(this.lang.get('_delete')); + this.image.buttonSave = this.modal.createActionButton(this.lang.get('save')); + + this.image.buttonDelete.on('click', $.proxy(function() { - return this.$element.attr('placeholder', this.opts.placeholder); + this.image.remove($image); + + }, this)); + + this.image.buttonSave.on('click', $.proxy(function() + { + this.image.update($image); + + }, this)); + + + $('#redactor-image-title').val($image.attr('alt')); + + if (!this.opts.imageLink) $('.redactor-image-link-option').hide(); + else + { + var $redactorImageLink = $('#redactor-image-link'); + + $redactorImageLink.attr('href', $image.attr('src')); + if ($link.size() !== 0) + { + $redactorImageLink.val($link.attr('href')); + if ($link.attr('target') == '_blank') $('#redactor-image-link-blank').prop('checked', true); + } } + + if (!this.opts.imagePosition) $('.redactor-image-position-option').hide(); else { - return !(typeof this.$element.attr('placeholder') == 'undefined' || this.$element.attr('placeholder') === ''); + var floatValue = ($image.css('display') == 'block' && $image.css('float') == 'none') ? 'center' : $image.css('float'); + $('#redactor-image-align').val(floatValue); } - } - }; - }, - autosave: function() - { - return { - enable: function() + + this.modal.show(); + + }, + setFloating: function($image) { - if (!this.opts.autosave) return; + var floating = $('#redactor-image-align').val(); - this.autosave.html = false; - this.autosave.name = (this.opts.autosaveName) ? this.opts.autosaveName : this.$textarea.attr('name'); + var imageFloat = ''; + var imageDisplay = ''; + var imageMargin = ''; - if (!this.opts.autosaveOnChange) + switch (floating) { - this.autosaveInterval = setInterval($.proxy(this.autosave.load, this), this.opts.autosaveInterval * 1000); + case 'left': + imageFloat = 'left'; + imageMargin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0'; + break; + case 'right': + imageFloat = 'right'; + imageMargin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin; + break; + case 'center': + imageDisplay = 'block'; + imageMargin = 'auto'; + break; } - }, - onChange: function() - { - if (!this.opts.autosaveOnChange) return; - this.autosave.load(); + $image.css({ 'float': imageFloat, display: imageDisplay, margin: imageMargin }); + $image.attr('rel', $image.attr('style')); }, - load: function() + update: function($image) { - var html = this.code.get(); - if (this.autosave.html === html) return; - if (this.utils.isEmpty(html)) return; + this.image.hideResize(); + this.buffer.set(); - $.ajax({ - url: this.opts.autosave, - type: 'post', - data: 'name=' + this.autosave.name + '&' + this.autosave.name + '=' + escape(encodeURIComponent(html)), - success: $.proxy(function(data) + var $link = $image.closest('a'); + + $image.attr('alt', $('#redactor-image-title').val()); + + this.image.setFloating($image); + + // as link + var link = $.trim($('#redactor-image-link').val()); + if (link !== '') + { + var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false; + + if ($link.size() === 0) { - this.autosave.success(data, html); + var a = $('<a href="' + link + '">' + this.utils.getOuterHtml($image) + '</a>'); + if (target) a.attr('target', '_blank'); - }, this) - }); + $image.replaceWith(a); + } + else + { + $link.attr('href', link); + if (target) + { + $link.attr('target', '_blank'); + } + else + { + $link.removeAttr('target'); + } + } + } + else if ($link.size() !== 0) + { + $link.replaceWith(this.utils.getOuterHtml($image)); + + } + + this.modal.close(); + this.observe.images(); + this.code.sync(); + + }, - success: function(data, html) + setEditable: function($image) { - var json; - try + if (this.opts.imageEditable) { - json = $.parseJSON(data); + $image.on('dragstart', $.proxy(this.image.onDrag, this)); } - catch(e) + + $image.on('mousedown', $.proxy(this.image.hideResize, this)); + $image.on('click touchstart', $.proxy(function(e) { - //data has already been parsed - json = data; - } + this.observe.image = $image; - var callbackName = (typeof json.error == 'undefined') ? 'autosave' : 'autosaveError'; + if (this.$editor.find('#redactor-image-box').size() !== 0) return false; - this.core.setCallback(callbackName, this.autosave.name, json); - this.autosave.html = html; + this.image.resizer = this.image.loadEditableControls($image); + + $(document).on('click.redactor-image-resize-hide', $.proxy(this.image.hideResize, this)); + this.$editor.on('click.redactor-image-resize-hide', $.proxy(this.image.hideResize, this)); + + // resize + if (!this.opts.imageResizable) return; + + this.image.resizer.on('mousedown.redactor touchstart.redactor', $.proxy(function(e) + { + this.image.setResizable(e, $image); + }, this)); + + + }, this)); }, - disable: function() + setResizable: function(e, $image) { - clearInterval(this.autosaveInterval); - } - }; - }, - buffer: function() - { - return { - set: function(type) + e.preventDefault(); + + this.image.resizeHandle = { + x : e.pageX, + y : e.pageY, + el : $image, + ratio: $image.width() / $image.height(), + h: $image.height() + }; + + e = e.originalEvent || e; + + if (e.targetTouches) + { + this.image.resizeHandle.x = e.targetTouches[0].pageX; + this.image.resizeHandle.y = e.targetTouches[0].pageY; + } + + this.image.startResize(); + + + }, + startResize: function() { - if (typeof type == 'undefined' || type == 'undo') + $(document).on('mousemove.redactor-image-resize touchmove.redactor-image-resize', $.proxy(this.image.moveResize, this)); + $(document).on('mouseup.redactor-image-resize touchend.redactor-image-resize', $.proxy(this.image.stopResize, this)); + }, + moveResize: function(e) + { + e.preventDefault(); + + e = e.originalEvent || e; + + var height = this.image.resizeHandle.h; + + if (e.targetTouches) height += (e.targetTouches[0].pageY - this.image.resizeHandle.y); + else height += (e.pageY - this.image.resizeHandle.y); + + var width = Math.round(height * this.image.resizeHandle.ratio); + + if (height < 50 || width < 100) return; + + this.image.resizeHandle.el.width(width); + this.image.resizeHandle.el.height(this.image.resizeHandle.el.width()/this.image.resizeHandle.ratio); + + this.code.sync(); + }, + stopResize: function() + { + this.handle = false; + $(document).off('.redactor-image-resize'); + + this.image.hideResize(); + }, + onDrag: function(e) + { + if (this.$editor.find('#redactor-image-box').size() !== 0) { - this.buffer.setUndo(); + e.preventDefault(); + return false; } - else + + this.$editor.on('drop.redactor-image-inside-drop', $.proxy(function() { - this.buffer.setRedo(); - } + setTimeout($.proxy(this.image.onDrop, this), 1); + + }, this)); }, - setUndo: function() + onDrop: function() { - this.selection.save(); - this.opts.buffer.push(this.$editor.html()); - this.selection.restore(); + this.image.fixImageSourceAfterDrop(); + this.observe.images(); + this.$editor.off('drop.redactor-image-inside-drop'); + this.clean.clearUnverified(); + this.code.sync(); }, - setRedo: function() + fixImageSourceAfterDrop: function() { - this.selection.save(); - this.opts.rebuffer.push(this.$editor.html()); - this.selection.restore(); + this.$editor.find('img[data-save-url]').each(function() + { + var $el = $(this); + $el.attr('src', $el.attr('data-save-url')); + $el.removeAttr('data-save-url'); + }); }, - getUndo: function() + hideResize: function(e) { - this.$editor.html(this.opts.buffer.pop()); + if (e && $(e.target).closest('#redactor-image-box').length !== 0) return; + if (e && e.target.tagName == 'IMG') + { + var $image = $(e.target); + $image.attr('data-save-url', $image.attr('src')); + } + + var imageBox = this.$editor.find('#redactor-image-box'); + if (imageBox.size() === 0) return; + + if (this.opts.imageEditable) + { + this.image.editter.remove(); + } + + $(this.image.resizer).remove(); + + imageBox.find('img').css({ + marginTop: imageBox[0].style.marginTop, + marginBottom: imageBox[0].style.marginBottom, + marginLeft: imageBox[0].style.marginLeft, + marginRight: imageBox[0].style.marginRight + }); + + imageBox.css('margin', ''); + imageBox.find('img').css('opacity', ''); + imageBox.replaceWith(function() + { + return $(this).contents(); + }); + + $(document).off('click.redactor-image-resize-hide'); + this.$editor.off('click.redactor-image-resize-hide'); + + if (typeof this.image.resizeHandle !== 'undefined') + { + this.image.resizeHandle.el.attr('rel', this.image.resizeHandle.el.attr('style')); + } + + this.code.sync(); + }, - getRedo: function() + loadResizableControls: function($image, imageBox) { - this.$editor.html(this.opts.rebuffer.pop()); + if (this.opts.imageResizable && !this.utils.isMobile()) + { + var imageResizer = $('<span id="redactor-image-resizer" data-redactor="verified"></span>'); + + if (!this.utils.isDesktop()) + { + imageResizer.css({ width: '15px', height: '15px' }); + } + + imageResizer.attr('contenteditable', false); + imageBox.append(imageResizer); + imageBox.append($image); + + return imageResizer; + } + else + { + imageBox.append($image); + return false; + } }, - add: function() + loadEditableControls: function($image) { - this.opts.buffer.push(this.$editor.html()); + var imageBox = $('<span id="redactor-image-box" data-redactor="verified">'); + imageBox.css('float', $image.css('float')).attr('contenteditable', false); + + if ($image[0].style.margin != 'auto') + { + imageBox.css({ + marginTop: $image[0].style.marginTop, + marginBottom: $image[0].style.marginBottom, + marginLeft: $image[0].style.marginLeft, + marginRight: $image[0].style.marginRight + }); + + $image.css('margin', ''); + } + else + { + imageBox.css({ 'display': 'block', 'margin': 'auto' }); + } + + $image.css('opacity', '.5').after(imageBox); + + + if (this.opts.imageEditable) + { + // editter + this.image.editter = $('<span id="redactor-image-editter" data-redactor="verified">' + this.lang.get('edit') + '</span>'); + this.image.editter.attr('contenteditable', false); + this.image.editter.on('click', $.proxy(function() + { + this.image.showEdit($image); + }, this)); + + imageBox.append(this.image.editter); + + // position correction + var editerWidth = this.image.editter.innerWidth(); + this.image.editter.css('margin-left', '-' + editerWidth/2 + 'px'); + } + + return this.image.loadResizableControls($image, imageBox); + }, - undo: function() + remove: function(image) { - if (this.opts.buffer.length === 0) return; + var $image = $(image); + var $link = $image.closest('a'); + var $figure = $image.closest('figure'); + var $parent = $image.parent(); + if ($('#redactor-image-box').size() !== 0) + { + $parent = $('#redactor-image-box').parent(); + } - this.buffer.set('redo'); - this.buffer.getUndo(); + var $next; + if ($figure.size() !== 0) + { + $next = $figure.next(); + $figure.remove(); + } + else if ($link.size() !== 0) + { + $parent = $link.parent(); + $link.remove(); + } + else + { + $image.remove(); + } - this.selection.restore(); + $('#redactor-image-box').remove(); - setTimeout($.proxy(this.observe.load, this), 50); + if ($figure.size() !== 0) + { + this.caret.setStart($next); + } + else + { + this.caret.setStart($parent); + } + + // delete callback + this.core.setCallback('imageDelete', $image[0].src, $image); + + this.modal.close(); + this.code.sync(); }, - redo: function() + insert: function(json, direct, e) { - if (this.opts.rebuffer.length === 0) return; + // error callback + if (typeof json.error != 'undefined') + { + this.modal.close(); + this.selection.restore(); + this.core.setCallback('imageUploadError', json); + return; + } - this.buffer.set('undo'); - this.buffer.getRedo(); + var $img; + if (typeof json == 'string') + { + $img = $(json).attr('data-redactor-inserted-image', 'true'); + } + else + { + $img = $('<img>'); + $img.attr('src', json.filelink).attr('data-redactor-inserted-image', 'true'); + } + + var node = $img; + var isP = this.utils.isCurrentOrParent('P'); + if (isP) + { + // will replace + node = $('<blockquote />').append($img); + } + + if (direct) + { + this.selection.removeMarkers(); + var marker = this.selection.getMarker(); + this.insert.nodeToCaretPositionFromPoint(e, marker); + } + else + { + this.modal.close(); + } + this.selection.restore(); + this.buffer.set(); - setTimeout($.proxy(this.observe.load, this), 50); + + this.insert.html(this.utils.getOuterHtml(node), false); + + var $image = this.$editor.find('img[data-redactor-inserted-image=true]').removeAttr('data-redactor-inserted-image'); + + if (isP) + { + $image.parent().contents().unwrap().wrap('<p />'); + } + else if (this.opts.linebreaks) + { + $image.before('<br>').after('<br>'); + } + + if (typeof json == 'string') return; + + this.core.setCallback('imageUpload', $image, json); + } }; }, indent: function() { @@ -3285,145 +3680,624 @@ $block.append('<br>'); } } }; }, - alignment: function() + inline: function() { return { - left: function() + formatting: function(name) { - this.alignment.set(''); + var type, value; + + if (typeof this.formatting[name].style != 'undefined') type = 'style'; + else if (typeof this.formatting[name].class != 'undefined') type = 'class'; + + if (type) value = this.formatting[name][type]; + + this.inline.format(this.formatting[name].tag, type, value); + }, - right: function() + format: function(tag, type, value) { - this.alignment.set('right'); + // Stop formatting pre and headers + if (this.utils.isCurrentOrParent('PRE') || this.utils.isCurrentOrParentHeader()) return; + + var tags = ['b', 'bold', 'i', 'italic', 'underline', 'strikethrough', 'deleted', 'superscript', 'subscript']; + var replaced = ['strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub']; + + for (var i = 0; i < tags.length; i++) + { + if (tag == tags[i]) tag = replaced[i]; + } + + this.inline.type = type || false; + this.inline.value = value || false; + + this.buffer.set(); + this.$editor.focus(); + + this.selection.get(); + + if (this.range.collapsed) + { + this.inline.formatCollapsed(tag); + } + else + { + this.inline.formatMultiple(tag); + } }, - center: function() + formatCollapsed: function(tag) { - this.alignment.set('center'); + var current = this.selection.getCurrent(); + var $parent = $(current).closest(tag + '[data-redactor-tag=' + tag + ']'); + + // inline there is + if ($parent.size() !== 0) + { + this.caret.setAfter($parent[0]); + + // remove empty + if (this.utils.isEmpty($parent.text())) $parent.remove(); + + this.code.sync(); + + return; + } + + // create empty inline + var node = $('<' + tag + '>').attr('data-verified', 'redactor').attr('data-redactor-tag', tag); + node.html(this.opts.invisibleSpace); + + node = this.inline.setFormat(node); + + var node = this.insert.node(node); + this.caret.setEnd(node); + + this.code.sync(); }, - justify: function() + formatMultiple: function(tag) { - this.alignment.set('justify'); - }, - set: function(type) - { - if (!this.utils.browser('msie')) this.$editor.focus(); + this.inline.formatConvert(tag); - this.buffer.set(); this.selection.save(); + document.execCommand('strikethrough'); - this.alignment.blocks = this.selection.getBlocks(); - if (this.opts.linebreaks && this.alignment.blocks[0] === false) + + this.$editor.find('strike').each($.proxy(function(i,s) { - this.alignment.setText(type); + var $el = $(s); + + this.inline.formatRemoveSameChildren($el, tag); + + var $span; + if (this.inline.type) + { + $span = $('<span>').attr('data-redactor-tag', tag).attr('data-verified', 'redactor'); + $span = this.inline.setFormat($span); + } + else + { + $span = $('<' + tag + '>').attr('data-redactor-tag', tag).attr('data-verified', 'redactor'); + } + + $el.replaceWith($span.html($el.contents())); + + if (tag == 'span') + { + var $parent = $span.parent(); + if ($parent && $parent[0].tagName == 'SPAN' && this.inline.type == 'style') + { + var arr = this.inline.value.split(';'); + + for (var z = 0; z < arr.length; z++) + { + if (arr[z] === '') return; + var style = arr[z].split(':'); + $parent.css(style[0], ''); + + if (this.utils.removeEmptyAttr($parent, 'style')) + { + $parent.replaceWith($parent.contents()); + } + + } + + } + } + + }, this)); + + // clear text decoration + if (tag != 'span') + { + this.$editor.find(this.opts.inlineTags.join(', ')).each($.proxy(function(i,s) + { + var $el = $(s); + var property = $el.css('text-decoration'); + if (property == 'line-through') + { + $el.css('text-decoration', ''); + this.utils.removeEmptyAttr($el, 'style'); + } + }, this)); } - else + + if (tag != 'del') { - this.alignment.setBlocks(type); + var _this = this; + this.$editor.find('inline').each(function(i,s) + { + _this.utils.replaceToTag(s, 'del'); + }); } this.selection.restore(); this.code.sync(); + }, - setText: function(type) + formatRemoveSameChildren: function($el, tag) { - var wrapper = this.selection.wrap('div'); - $(wrapper).attr('data-tagblock', 'redactor'); - $(wrapper).css('text-align', type); + $el.children(tag).each(function() + { + var $child = $(this); + if (!$child.hasClass('redactor-selection-marker')) + { + $child.contents().unwrap(); + } + }); }, - setBlocks: function(type) + formatConvert: function(tag) { - $.each(this.alignment.blocks, $.proxy(function(i, el) + this.selection.save(); + + var find = ''; + if (this.inline.type == 'class') find = '[data-redactor-class=' + this.inline.value + ']'; + else if (this.inline.type == 'style') { - var $el = this.utils.getAlignmentElement(el); + find = '[data-redactor-style="' + this.inline.value + '"]'; + } - if (!$el) return; + if (tag != 'del') + { + var self = this; + this.$editor.find('del').each(function(i,s) + { + self.utils.replaceToTag(s, 'inline'); + }); + } - if (type === '' && typeof($el.data('tagblock')) !== 'undefined') + this.$editor.find('[data-redactor-tag="' + tag + '"]' + find).each(function() + { + if (find === '' && tag == 'span' && this.tagName.toLowerCase() == tag) return; + + var $el = $(this); + $el.replaceWith($('<strike />').html($el.contents())); + + }); + + this.selection.restore(); + }, + setFormat: function(node) + { + switch (this.inline.type) + { + case 'class': + + if (node.hasClass(this.inline.value)) + { + node.removeClass(this.inline.value); + node.removeAttr('data-redactor-class'); + } + else + { + node.addClass(this.inline.value); + node.attr('data-redactor-class', this.inline.value); + } + + + break; + case 'style': + + node[0].style.cssText = this.inline.value; + node.attr('data-redactor-style', this.inline.value); + + break; + } + + return node; + }, + removeStyle: function() + { + this.buffer.set(); + var current = this.selection.getCurrent(); + var nodes = this.selection.getInlines(); + + this.selection.save(); + + if (current && current.tagName === 'SPAN') + { + var $s = $(current); + + $s.removeAttr('style'); + if ($s[0].attributes.length === 0) { - $el.replaceWith($el.html()); + $s.replaceWith($s.contents()); } - else + } + + $.each(nodes, $.proxy(function(i,s) + { + var $s = $(s); + if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) { - $el.css('text-align', type); - this.utils.removeEmptyAttr($el, 'style'); + $s.removeAttr('style'); + if ($s[0].attributes.length === 0) + { + $s.replaceWith($s.contents()); + } } + }, this)); + this.selection.restore(); + this.code.sync(); + }, + removeStyleRule: function(name) + { + this.buffer.set(); + var parent = this.selection.getParent(); + var nodes = this.selection.getInlines(); + + this.selection.save(); + + if (parent && parent.tagName === 'SPAN') + { + var $s = $(parent); + + $s.css(name, ''); + this.utils.removeEmptyAttr($s, 'style'); + if ($s[0].attributes.length === 0) + { + $s.replaceWith($s.contents()); + } + } + + $.each(nodes, $.proxy(function(i,s) + { + var $s = $(s); + if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) + { + $s.css(name, ''); + this.utils.removeEmptyAttr($s, 'style'); + if ($s[0].attributes.length === 0) + { + $s.replaceWith($s.contents()); + } + } }, this)); + + this.selection.restore(); + this.code.sync(); + }, + removeFormat: function() + { + this.buffer.set(); + var current = this.selection.getCurrent(); + + this.selection.save(); + + document.execCommand('removeFormat'); + + if (current && current.tagName === 'SPAN') + { + $(current).replaceWith($(current).contents()); + } + + + $.each(this.selection.getNodes(), $.proxy(function(i,s) + { + var $s = $(s); + if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) + { + $s.replaceWith($s.contents()); + } + }, this)); + + this.selection.restore(); + this.code.sync(); + + }, + toggleClass: function(className) + { + this.inline.format('span', 'class', className); + }, + toggleStyle: function(value) + { + this.inline.format('span', 'style', value); } }; }, - paste: function() + insert: function() { return { - init: function(e) + set: function(html, clean) { - if (!this.opts.cleanOnPaste) return; + this.placeholder.remove(); - this.rtePaste = true; + html = this.clean.setVerified(html); - this.buffer.set(); - this.selection.save(); - this.utils.saveScroll(); + if (typeof clean == 'undefined') + { + html = this.clean.onPaste(html, false); + } - this.paste.createPasteBox(); + this.$editor.html(html); + this.selection.remove(); + this.focus.setEnd(); + this.clean.normalizeLists(); + this.code.sync(); + this.observe.load(); - $(window).on('scroll.redactor-freeze', $.proxy(function() + if (typeof clean == 'undefined') { - $(window).scrollTop(this.saveBodyScroll); + setTimeout($.proxy(this.clean.clearUnverified, this), 10); + } + }, + text: function(text) + { + this.placeholder.remove(); - }, this)); + text = text.toString(); + text = $.trim(text); + text = this.clean.getPlainText(text, false); - setTimeout($.proxy(function() + this.$editor.focus(); + + if (this.utils.browser('msie')) { - var html = this.$pasteBox.html(); + this.insert.htmlIe(text); + } + else + { + this.selection.get(); - this.$pasteBox.remove(); + this.range.deleteContents(); + var el = document.createElement("div"); + el.innerHTML = text; + var frag = document.createDocumentFragment(), node, lastNode; + while ((node = el.firstChild)) + { + lastNode = frag.appendChild(node); + } - this.selection.restore(); - this.utils.restoreScroll(); + this.range.insertNode(frag); - this.paste.insert(html); + if (lastNode) + { + var range = this.range.cloneRange(); + range.setStartAfter(lastNode); + range.collapse(true); + this.sel.removeAllRanges(); + this.sel.addRange(range); + } + } - $(window).off('scroll.redactor-freeze'); + this.code.sync(); + this.clean.clearUnverified(); + }, + htmlWithoutClean: function(html) + { + this.insert.html(html, false); + }, + html: function(html, clean) + { + this.placeholder.remove(); - }, this), 1); + if (typeof clean == 'undefined') clean = true; + this.$editor.focus(); + + html = this.clean.setVerified(html); + + if (clean) + { + html = this.clean.onPaste(html); + } + + if (this.utils.browser('msie')) + { + this.insert.htmlIe(html); + } + else + { + if (this.clean.singleLine) this.insert.execHtml(html); + else document.execCommand('insertHTML', false, html); + + this.insert.htmlFixMozilla(); + + } + + this.clean.normalizeLists(); + + // remove empty paragraphs finaly + if (!this.opts.linebreaks) + { + this.$editor.find('p').each($.proxy(this.utils.removeEmpty, this)); + } + + this.code.sync(); + this.observe.load(); + + if (clean) + { + this.clean.clearUnverified(); + } + }, - createPasteBox: function() + htmlFixMozilla: function() { - this.$pasteBox = $('<div>').html(' ').attr('contenteditable', 'true').css({ position: 'fixed', width: 0, top: 0, left: '-9999px' }); + // FF inserts empty p when content was selected dblclick + if (!this.utils.browser('mozilla')) return; - $(document.body).append(this.$pasteBox); - this.$pasteBox.focus(); + var $next = $(this.selection.getBlock()).next(); + if ($next.length > 0 && $next[0].tagName == 'P' && $next.html() === '') + { + $next.remove(); + } + }, - insert: function(html) + htmlIe: function(html) { - html = this.core.setCallback('pasteBefore', html); + if (this.utils.isIe11()) + { + var parent = this.utils.isCurrentOrParent('P'); + var $html = $('<div>').append(html); + var blocksMatch = $html.contents().is('p, :header, dl, ul, ol, div, table, td, blockquote, pre, address, section, header, footer, aside, article'); - // clean - html = (this.utils.isSelectAll()) ? this.clean.onPaste(html, false) : this.clean.onPaste(html); + if (parent && blocksMatch) this.insert.ie11FixInserting(parent, html); + else this.insert.ie11PasteFrag(html); - html = this.core.setCallback('paste', html); + return; + } - if (this.utils.isSelectAll()) + document.selection.createRange().pasteHTML(html); + + }, + execHtml: function(html) + { + html = this.clean.setVerified(html); + + this.selection.get(); + + this.range.deleteContents(); + + var el = document.createElement('div'); + el.innerHTML = html; + + var frag = document.createDocumentFragment(), node, lastNode; + while ((node = el.firstChild)) { - this.insert.set(html, false); + lastNode = frag.appendChild(node); } - else + + this.range.insertNode(frag); + + this.range.collapse(true); + this.caret.setAfter(lastNode); + + }, + node: function(node, deleteContents) + { + node = node[0] || node; + + var html = this.utils.getOuterHtml(node); + html = this.clean.setVerified(html); + + node = $(html)[0]; + + this.selection.get(); + + if (deleteContents !== false) { - this.insert.html(html, false); + this.range.deleteContents(); } - this.utils.disableSelectAll(); - this.rtePaste = false; + this.range.insertNode(node); + this.range.collapse(false); + this.selection.addRange(); - setTimeout($.proxy(this.clean.clearUnverified, this), 10); + return node; + }, + nodeToPoint: function(node, x, y) + { + node = node[0] || node; + this.selection.get(); + + var range; + if (document.caretPositionFromPoint) + { + var pos = document.caretPositionFromPoint(x, y); + + this.range.setStart(pos.offsetNode, pos.offset); + this.range.collapse(true); + this.range.insertNode(node); + } + else if (document.caretRangeFromPoint) + { + range = document.caretRangeFromPoint(x, y); + range.insertNode(node); + } + else if (typeof document.body.createTextRange != "undefined") + { + range = document.body.createTextRange(); + range.moveToPoint(x, y); + var endRange = range.duplicate(); + endRange.moveToPoint(x, y); + range.setEndPoint("EndToEnd", endRange); + range.select(); + } + }, + nodeToCaretPositionFromPoint: function(e, node) + { + node = node[0] || node; + + var range; + var x = e.clientX, y = e.clientY; + if (document.caretPositionFromPoint) + { + var pos = document.caretPositionFromPoint(x, y); + var sel = document.getSelection(); + range = sel.getRangeAt(0); + range.setStart(pos.offsetNode, pos.offset); + range.collapse(true); + range.insertNode(node); + } + else if (document.caretRangeFromPoint) + { + range = document.caretRangeFromPoint(x, y); + range.insertNode(node); + } + else if (typeof document.body.createTextRange != "undefined") + { + range = document.body.createTextRange(); + range.moveToPoint(x, y); + var endRange = range.duplicate(); + endRange.moveToPoint(x, y); + range.setEndPoint("EndToEnd", endRange); + range.select(); + } + + }, + ie11FixInserting: function(parent, html) + { + var node = document.createElement('span'); + node.className = 'redactor-ie-paste'; + this.insert.node(node); + + var parHtml = $(parent).html(); + + parHtml = '<p>' + parHtml.replace(/<span class="redactor-ie-paste"><\/span>/gi, '</p>' + html + '<p>') + '</p>'; + $(parent).replaceWith(parHtml); + }, + ie11PasteFrag: function(html) + { + this.selection.get(); + this.range.deleteContents(); + + var el = document.createElement("div"); + el.innerHTML = html; + + var frag = document.createDocumentFragment(), node, lastNode; + while ((node = el.firstChild)) + { + lastNode = frag.appendChild(node); + } + + this.range.insertNode(frag); } }; }, keydown: function() { @@ -3459,10 +4333,33 @@ { e.preventDefault(); return false; } + // ie and ff exit from table + if (this.opts.enterKey && (this.utils.browser('msie') || this.utils.browser('mozilla')) && (key === this.keyCode.DOWN || key === this.keyCode.RIGHT)) + { + var isEndOfTable = false; + var $table = false; + if (this.keydown.block && this.keydown.block.tagName === 'TD') + { + $table = $(this.keydown.block).closest('table'); + } + + if ($table && $table.find('td').last()[0] === this.keydown.block) + { + isEndOfTable = true; + } + + if (this.utils.isEndOfElement() && isEndOfTable) + { + var node = $(this.opts.emptyHtml); + $table.after(node); + this.caret.setStart(node); + } + } + // down if (this.opts.enterKey && key === this.keyCode.DOWN) { this.keydown.onArrowDown(); } @@ -3545,10 +4442,12 @@ } else if (!this.opts.linebreaks && !this.keydown.block) { return this.keydown.insertParagraph(e); } + + } // Shift+Enter or Ctrl+Enter if (key === this.keyCode.ENTER && (e.ctrlKey || e.shiftKey)) @@ -3691,20 +4590,20 @@ }, onShiftEnter: function(e) { this.buffer.set(); - if (this.keydown.blockquote && this.utils.isEndOfElement()) + if (this.utils.isEndOfElement()) { return this.keydown.insertDblBreakLine(e); } return this.keydown.insertBreakLine(e); }, onTab: function(e, key) { - if (!this.opts.tabFocus) return true; + if (!this.opts.tabKey) return true; if (this.utils.isEmpty(this.code.get()) && this.opts.tabAsSpaces === false) return true; e.preventDefault(); var node; @@ -3936,10 +4835,11 @@ this.keyup.current = this.selection.getCurrent(); this.keyup.parent = this.selection.getParent(); var $parent = this.utils.isRedactorParent($(this.keyup.parent).parent()); + // callback var keyupStop = this.core.setCallback('keyup', e); if (keyupStop === false) { e.preventDefault(); @@ -3950,18 +4850,25 @@ if (!this.opts.linebreaks && this.keyup.current.nodeType == 3 && this.keyup.current.length <= 1 && (this.keyup.parent === false || this.keyup.parent.tagName == 'BODY')) { this.keyup.replaceToParagraph(); } - if ($(this.keyup.parent).hasClass('redactor-invisible-space') && ($parent === false || $parent[0].tagName == 'BODY')) + // replace div after lists + if (!this.opts.linebreaks && this.utils.isRedactorParent(this.keyup.current) && this.keyup.current.tagName === 'DIV') { + this.keyup.replaceToParagraph(false); + } + + + if (!this.opts.linebreaks && $(this.keyup.parent).hasClass('redactor-invisible-space') && ($parent === false || $parent[0].tagName == 'BODY')) + { $(this.keyup.parent).contents().unwrap(); this.keyup.replaceToParagraph(); } // linkify - if (this.opts.convertLinks && (this.opts.convertUrlLinks || this.opts.convertImageLinks || this.opts.convertVideoLinks) && key === this.keyCode.ENTER) + if (this.keyup.isLinkify(key)) { this.formatLinkify(this.opts.linkProtocol, this.opts.convertLinks, this.opts.convertUrlLinks, this.opts.convertImageLinks, this.opts.convertVideoLinks, this.opts.linkSize); this.observe.load(); this.code.sync(); @@ -4001,14 +4908,28 @@ // if empty return this.keyup.formatEmpty(e); } }, - replaceToParagraph: function() + isLinkify: function(key) { + return this.opts.convertLinks && (this.opts.convertUrlLinks || this.opts.convertImageLinks || this.opts.convertVideoLinks) && key === this.keyCode.ENTER && !this.utils.isCurrentOrParent('PRE'); + }, + replaceToParagraph: function(clone) + { var $current = $(this.keyup.current); - var node = $('<p>').append($current.clone()); + + var node; + if (clone === false) + { + node = $('<p>').append($current.html()); + } + else + { + node = $('<p>').append($current.clone()); + } + $current.replaceWith(node); var next = $(node).next(); if (typeof(next[0]) !== 'undefined' && next[0].tagName == 'BR') { next.remove(); @@ -4041,111 +4962,20 @@ return false; } }; }, - shortcuts: function() + lang: function() { return { - init: function(e, key) + load: function() { - // disable browser's hot keys for bold and italic - if (!this.opts.shortcuts) - { - if ((e.ctrlKey || e.metaKey) && (key === 66 || key === 73)) e.preventDefault(); - return false; - } - - $.each(this.opts.shortcuts, $.proxy(function(str, command) - { - var keys = str.split(','); - var len = keys.length; - for (var i = 0; i < len; i++) - { - if (typeof keys[i] === 'string') - { - this.shortcuts.handler(e, $.trim(keys[i]), $.proxy(function() - { - var func; - if (command.func.search(/\./) != '-1') - { - func = command.func.split('.'); - if (typeof this[func[0]] != 'undefined') - { - this[func[0]][func[1]].apply(this, command.params); - } - } - else - { - this[command.func].apply(this, command.params); - } - - }, this)); - } - - } - - }, this)); + this.opts.curLang = this.opts.langs[this.opts.lang]; }, - handler: function(e, keys, origHandler) + get: function(name) { - // based on https://github.com/jeresig/jquery.hotkeys - var hotkeysSpecialKeys = - { - 8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", - 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=", - 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", - 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=", - 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'" - }; - - - var hotkeysShiftNums = - { - "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", - "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", - ".": ">", "/": "?", "\\": "|" - }; - - keys = keys.toLowerCase().split(" "); - var special = hotkeysSpecialKeys[e.keyCode], - character = String.fromCharCode( e.which ).toLowerCase(), - modif = "", possible = {}; - - $.each([ "alt", "ctrl", "meta", "shift"], function(index, specialKey) - { - if (e[specialKey + 'Key'] && special !== specialKey) - { - modif += specialKey + '+'; - } - }); - - - if (special) possible[modif + special] = true; - if (character) - { - possible[modif + character] = true; - possible[modif + hotkeysShiftNums[character]] = true; - - // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" - if (modif === "shift+") - { - possible[hotkeysShiftNums[character]] = true; - } - } - - for (var i = 0, len = keys.length; i < len; i++) - { - if (possible[keys[i]]) - { - e.preventDefault(); - return origHandler.apply(this, arguments); - } - } + return (typeof this.opts.curLang[name] != 'undefined') ? this.opts.curLang[name] : ''; } }; }, line: function() { @@ -4224,15 +5054,234 @@ node.removeAttr('id'); } } }; }, + link: function() + { + return { + show: function(e) + { + if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); + + this.modal.load('link', this.lang.get('link_insert'), 600); + + this.modal.createCancelButton(); + this.link.buttonInsert = this.modal.createActionButton(this.lang.get('insert')); + + this.selection.get(); + + this.link.getData(); + this.link.cleanUrl(); + + if (this.link.target == '_blank') $('#redactor-link-blank').prop('checked', true); + + this.link.$inputUrl = $('#redactor-link-url'); + this.link.$inputText = $('#redactor-link-url-text'); + + this.link.$inputText.val(this.link.text); + this.link.$inputUrl.val(this.link.url); + + this.link.buttonInsert.on('click', $.proxy(this.link.insert, this)); + + // hide link's tooltip + $('.redactor-link-tooltip').remove(); + + // show modal + this.selection.save(); + this.modal.show(); + this.link.$inputUrl.focus(); + }, + cleanUrl: function() + { + var thref = self.location.href.replace(/\/$/i, ''); + this.link.url = this.link.url.replace(thref, ''); + this.link.url = this.link.url.replace(/^\/#/, '#'); + this.link.url = this.link.url.replace('mailto:', ''); + + // remove host from href + if (!this.opts.linkProtocol) + { + var re = new RegExp('^(http|ftp|https)://' + self.location.host, 'i'); + this.link.url = this.link.url.replace(re, ''); + } + + }, + getData: function() + { + this.link.$node = false; + + var $el = $(this.selection.getCurrent()).closest('a'); + if ($el.size() !== 0 && $el[0].tagName === 'A') + { + this.link.$node = $el; + + this.link.url = $el.attr('href'); + this.link.text = $el.text(); + this.link.target = $el.attr('target'); + } + else + { + this.link.text = this.sel.toString(); + this.link.url = ''; + this.link.target = ''; + } + + }, + insert: function() + { + var target = ''; + var link = this.link.$inputUrl.val(); + var text = this.link.$inputText.val(); + + if ($.trim(link) === '') + { + this.link.$inputUrl.addClass('redactor-input-error').on('keyup', function() + { + $(this).removeClass('redactor-input-error'); + $(this).off('keyup'); + + }); + + return; + } + + // mailto + if (link.search('@') != -1 && /(http|ftp|https):\/\//i.test(link) === false) + { + link = 'mailto:' + link; + } + // url, not anchor + else if (link.search('#') !== 0) + { + if ($('#redactor-link-blank').prop('checked')) + { + target = '_blank'; + } + + // test url (add protocol) + var pattern = '((xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}'; + var re = new RegExp('^(http|ftp|https)://' + pattern, 'i'); + var re2 = new RegExp('^' + pattern, 'i'); + + if (link.search(re) == -1 && link.search(re2) === 0 && this.opts.linkProtocol) + { + link = this.opts.linkProtocol + '://' + link; + } + } + + this.link.set(text, link, target); + this.modal.close(); + }, + set: function(text, link, target) + { + text = $.trim(text.replace(/<|>/g, '')); + + this.selection.restore(); + + if (text === '' && link === '') return; + if (text === '' && link !== '') text = link; + + if (this.link.$node) + { + this.buffer.set(); + + this.link.$node.text(text).attr('href', link); + if (target !== '') + { + this.link.$node.attr('target', target); + } + else + { + this.link.$node.removeAttr('target'); + } + + this.code.sync(); + } + else + { + if (this.utils.browser('mozilla') && this.link.text === '') + { + var $a = $('<a />').attr('href', link).text(text); + if (target !== '') $a.attr('target', target); + + this.insert.node($a); + this.selection.selectElement($a); + } + else + { + var $a; + if (this.utils.browser('msie')) + { + $a = $('<a href="' + link + '">').text(text); + if (target !== '') $a.attr('target', target); + + $a = $(this.insert.node($a)); + this.selection.selectElement($a); + } + else + { + document.execCommand('createLink', false, link); + + $a = $(this.selection.getCurrent()).closest('a'); + + if (target !== '') $a.attr('target', target); + $a.removeAttr('style'); + + if (this.link.text === '') + { + $a.text(text); + this.selection.selectElement($a); + } + } + } + + this.code.sync(); + this.core.setCallback('insertedLink', $a); + + } + + // link tooltip + setTimeout($.proxy(function() + { + this.observe.links(); + + }, this), 5); + }, + unlink: function(e) + { + if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); + + var nodes = this.selection.getNodes(); + if (!nodes) return; + + this.buffer.set(); + + var len = nodes.length; + for (var i = 0; i < len; i++) + { + if (nodes[i].tagName == 'A') + { + var $node = $(nodes[i]); + $node.replaceWith($node.contents()); + } + } + + // hide link's tooltip + $('.redactor-link-tooltip').remove(); + + this.code.sync(); + + } + }; + }, list: function() { return { toggle: function(cmd) { + this.placeholder.remove(); if (!this.utils.browser('msie')) this.$editor.focus(); this.buffer.set(); this.selection.save(); @@ -4279,22 +5328,42 @@ this.selection.restore(); this.code.sync(); }, insert: function(cmd) { + var parent = this.selection.getParent(); + var current = this.selection.getCurrent(); + var $td = $(current).closest('td, th'); + if (this.utils.browser('msie') && this.opts.linebreaks) { this.list.insertInIe(cmd); } else { document.execCommand('insert' + cmd); } - var parent = this.selection.getParent(); - var $list = $(parent).closest('ol, ul'); + var $list = $(this.selection.getParent()).closest('ol, ul'); + if ($td.size() !== 0) + { + var prev = $td.prev(); + var html = $td.html(); + $td.html(''); + if (prev && prev.length === 1 && (prev[0].tagName === 'TD' || prev[0].tagName === 'TH')) + { + $(prev).after($td); + } + else + { + $(parent).prepend($td); + } + + $td.html(html); + } + if (this.utils.isEmpty($list.find('li').text())) { var $children = $list.children('li'); $children.find('br').remove(); $children.append(this.selection.getMarkerAsHtml()); @@ -4362,11 +5431,11 @@ this.indent.fixEmptyIndent(); if (!this.opts.linebreaks && $current.closest('li, th, td').size() === 0) { document.execCommand('formatblock', false, 'p'); - this.$editor.find('ul, ol, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); + this.$editor.find('ul, ol, blockquote').each($.proxy(this.utils.removeEmpty, this)); } var $table = $(this.selection.getCurrent()).closest('table'); var $prev = $table.prev(); if (!this.opts.linebreaks && $table.size() !== 0 && $prev.size() !== 0 && $prev[0].tagName == 'BR') @@ -4377,1243 +5446,764 @@ this.clean.clearUnverified(); } }; }, - block: function() + modal: function() { return { - formatting: function(name) + callbacks: {}, + loadTemplates: function() { - var type, value; + this.opts.modal = { + imageEdit: String() + + '<section id="redactor-modal-image-edit">' + + '<label>' + this.lang.get('title') + '</label>' + + '<input type="text" id="redactor-image-title" />' + + '<label class="redactor-image-link-option">' + this.lang.get('link') + '</label>' + + '<input type="text" id="redactor-image-link" class="redactor-image-link-option" />' + + '<label class="redactor-image-link-option"><input type="checkbox" id="redactor-image-link-blank"> ' + this.lang.get('link_new_tab') + '</label>' + + '<label class="redactor-image-position-option">' + this.lang.get('image_position') + '</label>' + + '<select class="redactor-image-position-option" id="redactor-image-align">' + + '<option value="none">' + this.lang.get('none') + '</option>' + + '<option value="left">' + this.lang.get('left') + '</option>' + + '<option value="center">' + this.lang.get('center') + '</option>' + + '<option value="right">' + this.lang.get('right') + '</option>' + + '</select>' + + '</section>', - if (typeof this.formatting[name].data != 'undefined') type = 'data'; - else if (typeof this.formatting[name].attr != 'undefined') type = 'attr'; - else if (typeof this.formatting[name].class != 'undefined') type = 'class'; + image: String() + + '<section id="redactor-modal-image-insert">' + + '<div id="redactor-modal-image-droparea"></div>' + + '</section>', - if (type) value = this.formatting[name][type]; + file: String() + + '<section id="redactor-modal-file-insert">' + + '<div id="redactor-modal-file-upload-box">' + + '<label>' + this.lang.get('filename') + '</label>' + + '<input type="text" id="redactor-filename" /><br><br>' + + '<div id="redactor-modal-file-upload"></div>' + + '</div>' + + '</section>', - this.block.format(this.formatting[name].tag, type, value); + link: String() + + '<section id="redactor-modal-link-insert">' + + '<label>URL</label>' + + '<input type="url" id="redactor-link-url" />' + + '<label>' + this.lang.get('text') + '</label>' + + '<input type="text" id="redactor-link-url-text" />' + + '<label><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get('link_new_tab') + '</label>' + + '</section>' + }; - }, - format: function(tag, type, value) - { - if (tag == 'quote') tag = 'blockquote'; - var formatTags = ['p', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - if ($.inArray(tag, formatTags) == -1) return; + $.extend(this.opts, this.opts.modal); - this.block.isRemoveInline = (tag == 'pre' || tag.search(/h[1-6]/i) != -1); - - // focus - if (!this.utils.browser('msie')) this.$editor.focus(); - - this.block.blocks = this.selection.getBlocks(); - - this.block.blocksSize = this.block.blocks.length; - this.block.type = type; - this.block.value = value; - - this.buffer.set(); - this.selection.save(); - - this.block.set(tag); - - this.selection.restore(); - this.code.sync(); - }, - set: function(tag) + addCallback: function(name, callback) { - this.selection.get(); - this.block.containerTag = this.range.commonAncestorContainer.tagName; - - if (this.range.collapsed) - { - this.block.setCollapsed(tag); - } - else - { - this.block.setMultiple(tag); - } + this.modal.callbacks[name] = callback; }, - setCollapsed: function(tag) + createTabber: function($modal) { - var block = this.block.blocks[0]; - if (block === false) return; + this.modal.$tabber = $('<div>').attr('id', 'redactor-modal-tabber'); - if (block.tagName == 'LI') - { - if (tag != 'blockquote') return; - - this.block.formatListToBlockquote(); - return; - } - - var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); - if (isContainerTable) - { - if (!this.opts.linebreaks && tag == 'p') - { - document.execCommand('formatblock', false, '<' + tag + '>'); - - block = this.selection.getBlock(); - this.block.toggle($(block)); - } - } - else if (block.tagName.toLowerCase() != tag) - { - if (this.opts.linebreaks && tag == 'p') - { - $(block).prepend('<br>').append('<br>'); - this.utils.replaceWithContents(block); - } - else - { - var $formatted = this.utils.replaceToTag(block, tag); - - this.block.toggle($formatted); - - if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove(); - if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted); - if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap(); - } - } - else if (tag == 'blockquote' && block.tagName.toLowerCase() == tag) - { - // blockquote off - if (this.opts.linebreaks) - { - $(block).prepend('<br>').append('<br>'); - this.utils.replaceWithContents(block); - } - else - { - var $el = this.utils.replaceToTag(block, 'p'); - this.block.toggle($el); - } - } - else if (block.tagName.toLowerCase() == tag) - { - this.block.toggle($(block)); - } - + $modal.prepend(this.modal.$tabber); }, - setMultiple: function(tag) + addTab: function(id, name, active) { - var block = this.block.blocks[0]; - var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); - - if (block !== false && this.block.blocksSize === 1) + var $tab = $('<a href="#" rel="tab' + id + '">').text(name); + if (active) { - if (block.tagName.toLowerCase() == tag && tag == 'blockquote') - { - // blockquote off - if (this.opts.linebreaks) - { - $(block).prepend('<br>').append('<br>'); - this.utils.replaceWithContents(block); - } - else - { - var $el = this.utils.replaceToTag(block, 'p'); - this.block.toggle($el); - } - } - else if (block.tagName == 'LI') - { - if (tag != 'blockquote') return; - - this.block.formatListToBlockquote(); - } - else if (this.block.containerTag == 'BLOCKQUOTE') - { - this.block.formatBlockquote(tag); - } - else if (this.opts.linebreaks && ((isContainerTable) || (this.range.commonAncestorContainer != block))) - { - this.block.formatWrap(tag); - } - else - { - if (this.opts.linebreaks && tag == 'p') - { - $(block).prepend('<br>').append('<br>'); - this.utils.replaceWithContents(block); - } - else - { - - var $formatted = this.utils.replaceToTag(block, tag); - - this.block.toggle($formatted); - - if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted); - if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap(); - } - } + $tab.addClass('active'); } - else + + var self = this; + $tab.on('click', function(e) { - if (this.opts.linebreaks || tag != 'p') - { - if (tag == 'blockquote') - { - var count = 0; - for (var i = 0; i < this.block.blocksSize; i++) - { - if (this.block.blocks[i].tagName == 'BLOCKQUOTE') count++; - } + e.preventDefault(); + $('.redactor-tab').hide(); + $('.redactor-' + $(this).attr('rel')).show(); - // only blockquote selected - if (count == this.block.blocksSize) - { - $.each(this.block.blocks, $.proxy(function(i,s) - { - if (this.opts.linebreaks) - { - $(s).prepend('<br>').append('<br>'); - this.utils.replaceWithContents(s); - } - else - { - this.utils.replaceToTag(s, 'p'); - } + self.modal.$tabber.find('a').removeClass('active'); + $(this).addClass('active'); - }, this)); + }); - return; - } - - } - - this.block.formatWrap(tag); - } - else - { - var classSize = 0; - var toggleType = false; - if (this.block.type == 'class') - { - toggleType = 'toggle'; - classSize = $(this.block.blocks).filter('.' + this.block.value).size(); - - if (this.block.blocksSize == classSize) toggleType = 'toggle'; - else if (this.block.blocksSize > classSize) toggleType = 'set'; - else if (classSize === 0) toggleType = 'set'; - - } - - var exceptTags = ['ul', 'ol', 'li', 'td', 'th', 'dl', 'dt', 'dd']; - $.each(this.block.blocks, $.proxy(function(i,s) - { - if ($.inArray(s.tagName.toLowerCase(), exceptTags) != -1) return; - - var $formatted = this.utils.replaceToTag(s, tag); - - if (toggleType) - { - if (toggleType == 'toggle') this.block.toggle($formatted); - else if (toggleType == 'remove') this.block.remove($formatted); - else if (toggleType == 'set') this.block.set2($formatted); - } - else this.block.toggle($formatted); - - if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove(); - if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted); - if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap(); - - - }, this)); - } - } + this.modal.$tabber.append($tab); }, - toggle: function($el) + addTemplate: function(name, template) { - if (this.block.type == 'class') - { - $el.toggleClass(this.block.value); - return; - } - else if (this.block.type == 'attr' || this.block.type == 'data') - { - if ($el.attr(this.block.value.name) == this.block.value.value) - { - $el.removeAttr(this.block.value.name); - } - else - { - $el.attr(this.block.value.name, this.block.value.value); - } - - return; - } - else - { - $el.removeAttr('style class'); - return; - } + this.opts.modal[name] = template; }, - remove: function($el) + getTemplate: function(name) { - $el.removeClass(this.block.value); + return this.opts.modal[name]; }, - formatListToBlockquote: function() + getModal: function() { - var block = $(this.block.blocks[0]).closest('ul, ol'); - - $(block).find('ul, ol').contents().unwrap(); - $(block).find('li').append($('<br>')).contents().unwrap(); - - var $el = this.utils.replaceToTag(block, 'blockquote'); - this.block.toggle($el); + return this.$modalBody.find('section'); }, - formatBlockquote: function(tag) + load: function(templateName, title, width) { - document.execCommand('outdent'); - document.execCommand('formatblock', false, tag); + this.modal.templateName = templateName; + this.modal.width = width; - this.clean.clearUnverified(); - this.$editor.find('p:empty').remove(); + this.modal.build(); + this.modal.enableEvents(); + this.modal.setTitle(title); + this.modal.setDraggable(); + this.modal.setContent(); - var formatted = this.selection.getBlock(); - - if (tag != 'p') + // callbacks + if (typeof this.modal.callbacks[templateName] != 'undefined') { - $(formatted).find('img').remove(); + this.modal.callbacks[templateName].call(this); } - if (!this.opts.linebreaks) + }, + show: function() + { + // ios keyboard hide + if (this.utils.isMobile() && !this.utils.browser('msie')) { - this.block.toggle($(formatted)); + document.activeElement.blur(); } - this.$editor.find('ul, ol, tr, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); + $(document.body).removeClass('body-redactor-hidden'); + this.modal.bodyOveflow = $(document.body).css('overflow'); + $(document.body).css('overflow', 'hidden'); - if (this.opts.linebreaks && tag == 'p') + if (this.utils.isMobile()) { - this.utils.replaceWithContents(formatted); + this.modal.showOnMobile(); } - - }, - formatWrap: function(tag) - { - if (this.block.containerTag == 'UL' || this.block.containerTag == 'OL') + else { - if (tag == 'blockquote') - { - this.block.formatListToBlockquote(); - } - else - { - return; - } + this.modal.showOnDesktop(); } - var formatted = this.selection.wrap(tag); - if (formatted === false) return; + this.$modalOverlay.show(); + this.$modalBox.show(); - var $formatted = $(formatted); + this.modal.setButtonsWidth(); - this.block.formatTableWrapping($formatted); + this.utils.saveScroll(); - var $elements = $formatted.find(this.opts.blockLevelElements.join(',') + ', td, table, thead, tbody, tfoot, th, tr'); - - if ((this.opts.linebreaks && tag == 'p') || tag == 'pre' || tag == 'blockquote') + // resize + if (!this.utils.isMobile()) { - $elements.append('<br />'); + setTimeout($.proxy(this.modal.showOnDesktop, this), 0); + $(window).on('resize.redactor-modal', $.proxy(this.modal.resize, this)); } - $elements.contents().unwrap(); + // modal shown callback + this.core.setCallback('modalOpened', this.modal.templateName, this.$modal); - if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove(); + // fix bootstrap modal focus + $(document).off('focusin.modal'); - $.each(this.block.blocks, $.proxy(this.utils.removeEmpty, this)); + // enter + this.$modal.find('input[type=text]').on('keydown.redactor-modal', $.proxy(this.modal.setEnter, this)); - $formatted.append(this.selection.getMarker(2)); + }, + showOnDesktop: function() + { + var height = this.$modal.outerHeight(); + var windowHeight = $(window).height(); + var windowWidth = $(window).width(); - if (!this.opts.linebreaks) + if (this.modal.width > windowWidth) { - this.block.toggle($formatted); + this.$modal.css({ + width: '96%', + marginTop: (windowHeight/2 - height/2) + 'px' + }); + return; } - this.$editor.find('ul, ol, tr, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); - $formatted.find('blockquote:empty').remove(); - - if (this.block.isRemoveInline) + if (height > windowHeight) { - this.utils.removeInlineTags($formatted); + this.$modal.css({ + width: this.modal.width + 'px', + marginTop: '20px' + }); } - - if (this.opts.linebreaks && tag == 'p') + else { - this.utils.replaceWithContents($formatted); + this.$modal.css({ + width: this.modal.width + 'px', + marginTop: (windowHeight/2 - height/2) + 'px' + }); } - }, - formatTableWrapping: function($formatted) + showOnMobile: function() { - if ($formatted.closest('table').size() === 0) return; + this.$modal.css({ + width: '96%', + marginTop: '2%' + }); - if ($formatted.closest('tr').size() === 0) $formatted.wrap('<tr>'); - if ($formatted.closest('td').size() === 0) $formatted.wrap('<td>'); }, - removeData: function(name, value) + resize: function() { - var blocks = this.selection.getBlocks(); - $(blocks).removeAttr('data-' + name); - - this.code.sync(); + if (this.utils.isMobile()) + { + this.modal.showOnMobile(); + } + else + { + this.modal.showOnDesktop(); + } }, - setData: function(name, value) + setTitle: function(title) { - var blocks = this.selection.getBlocks(); - $(blocks).attr('data-' + name, value); - - this.code.sync(); + this.$modalHeader.html(title); }, - toggleData: function(name, value) + setContent: function() { - var blocks = this.selection.getBlocks(); - $.each(blocks, function() - { - if ($(this).attr('data-' + name)) - { - $(this).removeAttr('data-' + name); - } - else - { - $(this).attr('data-' + name, value); - } - }); + this.$modalBody.html(this.modal.getTemplate(this.modal.templateName)); }, - removeAttr: function(attr, value) + setDraggable: function() { - var blocks = this.selection.getBlocks(); - $(blocks).removeAttr(attr); + if (typeof $.fn.draggable === 'undefined') return; - this.code.sync(); + this.$modal.draggable({ handle: this.$modalHeader }); + this.$modalHeader.css('cursor', 'move'); }, - setAttr: function(attr, value) + setEnter: function(e) { - var blocks = this.selection.getBlocks(); - $(blocks).attr(attr, value); + if (e.which != 13) return; - this.code.sync(); + e.preventDefault(); + this.$modal.find('button.redactor-modal-action-btn').click(); }, - toggleAttr: function(attr, value) + createCancelButton: function() { - var blocks = this.selection.getBlocks(); - $.each(blocks, function() - { - if ($(this).attr(name)) - { - $(this).removeAttr(name); - } - else - { - $(this).attr(name, value); - } - }); + var button = $('<button>').addClass('redactor-modal-btn redactor-modal-close-btn').html(this.lang.get('cancel')); + button.on('click', $.proxy(this.modal.close, this)); + + this.$modalFooter.append(button); }, - removeClass: function(className) + createDeleteButton: function(label) { - var blocks = this.selection.getBlocks(); - $(blocks).removeClass(className); - - this.utils.removeEmptyAttr(blocks, 'class'); - - this.code.sync(); + return this.modal.createButton(label, 'delete'); }, - setClass: function(className) + createActionButton: function(label) { - var blocks = this.selection.getBlocks(); - $(blocks).addClass(className); - - this.code.sync(); + return this.modal.createButton(label, 'action'); }, - toggleClass: function(className) + createButton: function(label, className) { - var blocks = this.selection.getBlocks(); - $(blocks).toggleClass(className); + var button = $('<button>').addClass('redactor-modal-btn').addClass('redactor-modal-' + className + '-btn').html(label); + this.$modalFooter.append(button); - this.code.sync(); - } - }; - }, - inline: function() - { - return { - formatting: function(name) + return button; + }, + setButtonsWidth: function() { - var type, value; + var buttons = this.$modalFooter.find('button'); + var buttonsSize = buttons.size(); + if (buttonsSize === 0) return; - if (typeof this.formatting[name].style != 'undefined') type = 'style'; - else if (typeof this.formatting[name].class != 'undefined') type = 'class'; - - if (type) value = this.formatting[name][type]; - - this.inline.format(this.formatting[name].tag, type, value); - + buttons.css('width', (100/buttonsSize) + '%'); }, - format: function(tag, type, value) + build: function() { - // Stop formatting pre - if (this.utils.isCurrentOrParent('PRE')) return; + this.modal.buildOverlay(); - var tags = ['b', 'bold', 'i', 'italic', 'underline', 'strikethrough', 'deleted', 'superscript', 'subscript']; - var replaced = ['strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub']; + this.$modalBox = $('<div id="redactor-modal-box" />').hide(); + this.$modal = $('<div id="redactor-modal" />'); + this.$modalHeader = $('<header />'); + this.$modalClose = $('<span id="redactor-modal-close" />').html('&times;'); + this.$modalBody = $('<div id="redactor-modal-body" />'); + this.$modalFooter = $('<footer />'); - for (var i = 0; i < tags.length; i++) - { - if (tag == tags[i]) tag = replaced[i]; - } - - this.inline.type = type || false; - this.inline.value = value || false; - - this.buffer.set(); - this.$editor.focus(); - - this.selection.get(); - - if (this.range.collapsed) - { - this.inline.formatCollapsed(tag); - } - else - { - this.inline.formatMultiple(tag); - } + this.$modal.append(this.$modalHeader); + this.$modal.append(this.$modalClose); + this.$modal.append(this.$modalBody); + this.$modal.append(this.$modalFooter); + this.$modalBox.append(this.$modal); + this.$modalBox.appendTo(document.body); }, - formatCollapsed: function(tag) + buildOverlay: function() { - var current = this.selection.getCurrent(); - var $parent = $(current).closest(tag + '[data-redactor-tag=' + tag + ']'); - - // inline there is - if ($parent.size() !== 0) - { - this.caret.setAfter($parent[0]); - - // remove empty - if (this.utils.isEmpty($parent.text())) $parent.remove(); - - this.code.sync(); - - return; - } - - // create empty inline - var node = $('<' + tag + '>').attr('data-verified', 'redactor').attr('data-redactor-tag', tag); - node.html(this.opts.invisibleSpace); - - node = this.inline.setFormat(node); - - this.insert.node(node); - this.caret.setEnd(node); - - this.code.sync(); - - return; + this.$modalOverlay = $('<div id="redactor-modal-overlay">').hide(); + $('body').prepend(this.$modalOverlay); }, - formatMultiple: function(tag) + enableEvents: function() { - this.inline.formatConvert(tag); + this.$modalClose.on('click.redactor-modal', $.proxy(this.modal.close, this)); + $(document).on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this)); + this.$editor.on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this)); + this.$modalBox.on('click.redactor-modal', $.proxy(this.modal.close, this)); + }, + disableEvents: function() + { + this.$modalClose.off('click.redactor-modal'); + $(document).off('keyup.redactor-modal'); + this.$editor.off('keyup.redactor-modal'); + this.$modalBox.off('click.redactor-modal'); + $(window).off('resize.redactor-modal'); + }, + closeHandler: function(e) + { + if (e.which != this.keyCode.ESC) return; - this.selection.save(); - document.execCommand('strikethrough'); - - this.$editor.find('strike').each($.proxy(function(i,s) - { - var $el = $(s); - - this.inline.formatRemoveSameChildren($el, tag); - - var $span; - if (this.inline.type) - { - $span = $('<span>').attr('data-redactor-tag', tag).attr('data-verified', 'redactor'); - $span = this.inline.setFormat($span); - } - else - { - $span = $('<' + tag + '>').attr('data-redactor-tag', tag).attr('data-verified', 'redactor'); - } - - $el.replaceWith($span.html($el.contents())); - - if (tag == 'span') - { - var $parent = $span.parent(); - if ($parent && $parent[0].tagName == 'SPAN' && this.inline.type == 'style') - { - var arr = this.inline.value.split(';'); - - for (var z = 0; z < arr.length; z++) - { - if (arr[z] === '') return; - var style = arr[z].split(':'); - $parent.css(style[0], ''); - - if (this.utils.removeEmptyAttr($parent, 'style')) - { - $parent.replaceWith($parent.contents()); - } - - } - - } - } - - }, this)); - - if (tag != 'del') - { - var self = this; - this.$editor.find('inline').each(function(i,s) - { - self.utils.replaceToTag(s, 'del'); - }); - } - - this.selection.restore(); - this.code.sync(); - + this.modal.close(false); }, - formatRemoveSameChildren: function($el, tag) + close: function(e) { - $el.children(tag).each(function() + if (e) { - var $child = $(this); - if (!$child.hasClass('redactor-selection-marker')) + if (!$(e.target).hasClass('redactor-modal-close-btn') && e.target != this.$modalClose[0] && e.target != this.$modalBox[0]) { - $child.contents().unwrap(); + return; } - }); - }, - formatConvert: function(tag) - { - this.selection.save(); - var find = ''; - if (this.inline.type == 'class') find = '[data-redactor-class=' + this.inline.value + ']'; - else if (this.inline.type == 'style') - { - find = '[data-redactor-style="' + this.inline.value + '"]'; + e.preventDefault(); } - if (tag != 'del') - { - var self = this; - this.$editor.find('del').each(function(i,s) - { - self.utils.replaceToTag(s, 'inline'); - }); - } + if (!this.$modalBox) return; - this.$editor.find('[data-redactor-tag="' + tag + '"]' + find).each(function() - { - if (find === '' && tag == 'span' && this.tagName.toLowerCase() == tag) return; + this.modal.disableEvents(); - var $el = $(this); - $el.replaceWith($('<strike />').html($el.contents())); + this.$modalOverlay.remove(); - }); - - this.selection.restore(); - }, - setFormat: function(node) - { - switch (this.inline.type) + this.$modalBox.fadeOut('fast', $.proxy(function() { - case 'class': + this.$modalBox.remove(); - if (node.hasClass(this.inline.value)) - { - node.removeClass(this.inline.value); - node.removeAttr('data-redactor-class'); - } - else - { - node.addClass(this.inline.value); - node.attr('data-redactor-class', this.inline.value); - } + setTimeout($.proxy(this.utils.restoreScroll, this), 0); + if (e !== undefined) this.selection.restore(); - break; - case 'style': + $(document.body).css('overflow', this.modal.bodyOveflow); + this.core.setCallback('modalClosed', this.modal.templateName); - node[0].style.cssText = this.inline.value; - node.attr('data-redactor-style', this.inline.value); + }, this)); - break; - } - - return node; + } + }; + }, + observe: function() + { + return { + load: function() + { + this.observe.images(); + this.observe.links(); }, - removeStyle: function() + buttons: function(e, btnName) { - this.buffer.set(); var current = this.selection.getCurrent(); - var nodes = this.selection.getInlines(); + var parent = this.selection.getParent(); - this.selection.save(); + this.button.setInactiveAll(btnName); - if (current && current.tagName === 'SPAN') + if (e === false && btnName !== 'html') { - var $s = $(current); - - $s.removeAttr('style'); - if ($s[0].attributes.length === 0) - { - $s.replaceWith($s.contents()); - } + if ($.inArray(btnName, this.opts.activeButtons) != -1) this.button.toggleActive(btnName); + return; } - $.each(nodes, $.proxy(function(i,s) - { - var $s = $(s); - if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) - { - $s.removeAttr('style'); - if ($s[0].attributes.length === 0) - { - $s.replaceWith($s.contents()); - } - } - }, this)); + //var linkButtonName = (this.utils.isCurrentOrParent('A')) ? this.lang.get('link_edit') : this.lang.get('link_insert'); + //$('body').find('a.redactor-dropdown-link').text(linkButtonName); - this.selection.restore(); - this.code.sync(); - - }, - removeStyleRule: function(name) - { - this.buffer.set(); - var parent = this.selection.getParent(); - var nodes = this.selection.getInlines(); - - this.selection.save(); - - if (parent && parent.tagName === 'SPAN') + $.each(this.opts.activeButtonsStates, $.proxy(function(key, value) { - var $s = $(parent); + var parentEl = $(parent).closest(key); + var currentEl = $(current).closest(key); - $s.css(name, ''); - this.utils.removeEmptyAttr($s, 'style'); - if ($s[0].attributes.length === 0) + if (parentEl.length !== 0 && !this.utils.isRedactorParent(parentEl)) return; + if (!this.utils.isRedactorParent(currentEl)) return; + if (parentEl.length !== 0 || currentEl.closest(key).length !== 0) { - $s.replaceWith($s.contents()); + this.button.setActive(value); } - } - $.each(nodes, $.proxy(function(i,s) - { - var $s = $(s); - if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) - { - $s.css(name, ''); - this.utils.removeEmptyAttr($s, 'style'); - if ($s[0].attributes.length === 0) - { - $s.replaceWith($s.contents()); - } - } }, this)); - this.selection.restore(); - this.code.sync(); + var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase()); + if (this.utils.isRedactorParent(parent) && $parent.length) + { + var align = ($parent.css('text-align') === '') ? 'left' : $parent.css('text-align'); + this.button.setActive('align' + align); + } }, - removeFormat: function() + addButton: function(tag, btnName) { - this.buffer.set(); - var current = this.selection.getCurrent(); + this.opts.activeButtons.push(btnName); + this.opts.activeButtonsStates[tag] = btnName; + }, + images: function() + { + this.$editor.find('img').each($.proxy(function(i, img) + { + var $img = $(img); - this.selection.save(); + // IE fix (when we clicked on an image and then press backspace IE does goes to image's url) + $img.closest('a').on('click', function(e) { e.preventDefault(); }); - document.execCommand('removeFormat'); + if (this.utils.browser('msie')) $img.attr('unselectable', 'on'); - if (current && current.tagName === 'SPAN') - { - $(current).replaceWith($(current).contents()); - } + this.image.setEditable($img); + }, this)); - $.each(this.selection.getNodes(), $.proxy(function(i,s) + $(document).on('click.redactor-image-delete', $.proxy(function(e) { - var $s = $(s); - if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) + this.observe.image = false; + if (e.target.tagName == 'IMG' && this.utils.isRedactorParent(e.target)) { - $s.replaceWith($s.contents()); + this.observe.image = (this.observe.image && this.observe.image == e.target) ? false : e.target; } + }, this)); - this.selection.restore(); - this.code.sync(); + }, + links: function() + { + if (!this.opts.linkTooltip) return; + this.$editor.find('a').on('touchstart click', $.proxy(this.observe.showTooltip, this)); + this.$editor.on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this)); + $(document).on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this)); }, - toggleClass: function(className) + getTooltipPosition: function($link) { - this.inline.format('span', 'class', className); + return $link.offset(); }, - toggleStyle: function(value) + showTooltip: function(e) { - this.inline.format('span', 'style', value); - } - }; - }, - insert: function() - { - return { - set: function(html, clean) - { - this.placeholder.remove(); + var $link = $(e.target); + var $parent = $link.closest('a'); + var tag = ($link.size() !== 0) ? $link[0].tagName : false; - html = this.clean.setVerified(html); - - if (typeof clean == 'undefined') + if ($parent[0].tagName === 'A') { - html = this.clean.onPaste(html, false); + if (tag === 'IMG') return; + else if (tag !== 'A') $link = $parent; } - this.$editor.html(html); - this.selection.remove(); - this.focus.setEnd(); - this.clean.normalizeLists(); - this.code.sync(); - this.observe.load(); - - if (typeof clean == 'undefined') + if (tag !== 'A') { - setTimeout($.proxy(this.clean.clearUnverified, this), 10); + return; } - }, - text: function(text) - { - this.placeholder.remove(); - text = text.toString(); - text = $.trim(text); - text = this.clean.getPlainText(text, false); + var pos = this.observe.getTooltipPosition($link); + var tooltip = $('<span class="redactor-link-tooltip"></span>'); - this.$editor.focus(); - - if (this.utils.browser('msie')) + var href = $link.attr('href'); + if (href === undefined) { - this.insert.htmlIe(text); + href = ''; } - else - { - this.selection.get(); - this.range.deleteContents(); - var el = document.createElement("div"); - el.innerHTML = text; - var frag = document.createDocumentFragment(), node, lastNode; - while ((node = el.firstChild)) - { - lastNode = frag.appendChild(node); - } + if (href.length > 24) href = href.substring(0, 24) + '...'; - this.range.insertNode(frag); + var aLink = $('<a href="' + $link.attr('href') + '" target="_blank" />').html(href).addClass('redactor-link-tooltip-action'); + var aEdit = $('<a href="#" />').html(this.lang.get('edit')).on('click', $.proxy(this.link.show, this)).addClass('redactor-link-tooltip-action'); + var aUnlink = $('<a href="#" />').html(this.lang.get('unlink')).on('click', $.proxy(this.link.unlink, this)).addClass('redactor-link-tooltip-action'); - if (lastNode) - { - var range = this.range.cloneRange(); - range.setStartAfter(lastNode); - range.collapse(true); - this.sel.removeAllRanges(); - this.sel.addRange(range); - } - } + tooltip.append(aLink).append(' | ').append(aEdit).append(' | ').append(aUnlink); + tooltip.css({ + top: (pos.top + 20) + 'px', + left: pos.left + 'px' + }); - this.code.sync(); - this.clean.clearUnverified(); + $('.redactor-link-tooltip').remove(); + $('body').append(tooltip); }, - html: function(html, clean) + closeTooltip: function(e) { - this.placeholder.remove(); + e = e.originalEvent || e; - if (typeof clean == 'undefined') clean = true; - - this.$editor.focus(); - - html = this.clean.setVerified(html); - - if (clean) + var target = e.target; + var $parent = $(target).closest('a'); + if ($parent.size() !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A') { - html = this.clean.onPaste(html); + return; } - - if (this.utils.browser('msie')) + else if ((target.tagName === 'A' && this.utils.isRedactorParent(target)) || $(target).hasClass('redactor-link-tooltip-action')) { - this.insert.htmlIe(html); + return; } - else - { - if (this.clean.singleLine) this.insert.execHtml(html); - else document.execCommand('insertHTML', null, html); - this.insert.htmlFixMozilla(); - } + $('.redactor-link-tooltip').remove(); + } - this.clean.normalizeLists(); + }; + }, + paragraphize: function() + { + return { + load: function(html) + { + if (this.opts.linebreaks) return html; + if (html === '' || html === '<p></p>') return this.opts.emptyHtml; - // remove empty paragraphs finaly - if (!this.opts.linebreaks) - { - this.$editor.find('p').each($.proxy(this.utils.removeEmpty, this)); - } + this.paragraphize.blocks = ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption', + 'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea', + 'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p']; - this.code.sync(); - this.observe.load(); + html = html + "\n"; - if (clean) - { - this.clean.clearUnverified(); - } + this.paragraphize.safes = []; + this.paragraphize.z = 0; - }, - htmlFixMozilla: function() - { - // FF inserts empty p when content was selected dblclick - if (!this.utils.browser('mozilla')) return; + html = html.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>'); - var $next = $(this.selection.getBlock()).next(); - if ($next.length > 0 && $next[0].tagName == 'P' && $next.html() === '') - { - $next.remove(); - } + html = this.paragraphize.getSafes(html); + html = this.paragraphize.getSafesComments(html); + html = this.paragraphize.replaceBreaksToNewLines(html); + html = this.paragraphize.replaceBreaksToParagraphs(html); + html = this.paragraphize.clear(html); + html = this.paragraphize.restoreSafes(html); + html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.paragraphize.blocks.join('|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>'); + + return $.trim(html); }, - htmlIe: function(html) + getSafes: function(html) { - if (this.utils.isIe11()) + var $div = $('<div />').append(html); + + // remove paragraphs in blockquotes + $div.find('blockquote p').replaceWith(function() { - var parent = this.utils.isCurrentOrParent('P'); - var $html = $('<div>').append(html); - var blocksMatch = $html.contents().is('p, :header, dl, ul, ol, div, table, td, blockquote, pre, address, section, header, footer, aside, article'); + return $(this).append('<br />').contents(); + }); - if (parent && blocksMatch) this.insert.ie11FixInserting(parent, html); - else this.insert.ie11PasteFrag(html); + html = $div.html(); - return; - } + $div.find(this.paragraphize.blocks.join(', ')).each($.proxy(function(i,s) + { + this.paragraphize.z++; + this.paragraphize.safes[this.paragraphize.z] = s.outerHTML; + html = html.replace(s.outerHTML, '\n{replace' + this.paragraphize.z + '}'); - document.selection.createRange().pasteHTML(html); + }, this)); + return html; }, - execHtml: function(html) + getSafesComments: function(html) { - html = this.clean.setVerified(html); + var commentsMatches = html.match(/<!--([\w\W]*?)-->/gi); - this.selection.get(); + if (!commentsMatches) return html; - this.range.deleteContents(); - - var el = document.createElement('div'); - el.innerHTML = html; - - var frag = document.createDocumentFragment(), node, lastNode; - while ((node = el.firstChild)) + $.each(commentsMatches, $.proxy(function(i,s) { - lastNode = frag.appendChild(node); - } + this.paragraphize.z++; + this.paragraphize.safes[this.paragraphize.z] = s; + html = html.replace(s, '\n{replace' + this.paragraphize.z + '}'); + }, this)); - this.range.insertNode(frag); - - this.range.collapse(true); - this.caret.setAfter(lastNode); - + return html; }, - node: function(node) + restoreSafes: function(html) { - node = node[0] || node; + $.each(this.paragraphize.safes, function(i,s) + { + html = html.replace('{replace' + i + '}', s); + }); - this.selection.get(); - this.range.deleteContents(); - this.range.insertNode(node); - this.range.collapse(false); - this.selection.addRange(); - - return node; + return html; }, - nodeToPoint: function(node, x, y) + replaceBreaksToParagraphs: function(html) { - node = node[0] || node; + var htmls = html.split(new RegExp('\n', 'g'), -1); - this.selection.get(); - - var range; - if (document.caretPositionFromPoint) + html = ''; + if (htmls) { - var pos = document.caretPositionFromPoint(x, y); + var len = htmls.length; + for (var i = 0; i < len; i++) + { + if (!htmls.hasOwnProperty(i)) return; - this.range.setStart(pos.offsetNode, pos.offset); - this.range.collapse(true); - this.range.insertNode(node); - } - else if (document.caretRangeFromPoint) - { - range = document.caretRangeFromPoint(x, y); - range.insertNode(node); - } - else if (typeof document.body.createTextRange != "undefined") - { - range = document.body.createTextRange(); - range.moveToPoint(x, y); - var endRange = range.duplicate(); - endRange.moveToPoint(x, y); - range.setEndPoint("EndToEnd", endRange); - range.select(); - } - }, - nodeToCaretPositionFromPoint: function(e, node) - { - node = node[0] || node; + if (htmls[i].search('{replace') == -1) + { + htmls[i] = htmls[i].replace(/<p>\n\t?<\/p>/gi, ''); + htmls[i] = htmls[i].replace(/<p><\/p>/gi, ''); - var range; - var x = e.clientX, y = e.clientY; - if (document.caretPositionFromPoint) - { - var pos = document.caretPositionFromPoint(x, y); - var sel = document.getSelection(); - range = sel.getRangeAt(0); - range.setStart(pos.offsetNode, pos.offset); - range.collapse(true); - range.insertNode(node); + if (htmls[i] !== '') + { + html += '<p>' + htmls[i].replace(/^\n+|\n+$/g, "") + "</p>"; + } + } + else html += htmls[i]; + } } - else if (document.caretRangeFromPoint) - { - range = document.caretRangeFromPoint(x, y); - range.insertNode(node); - } - else if (typeof document.body.createTextRange != "undefined") - { - range = document.body.createTextRange(); - range.moveToPoint(x, y); - var endRange = range.duplicate(); - endRange.moveToPoint(x, y); - range.setEndPoint("EndToEnd", endRange); - range.select(); - } + return html; }, - ie11FixInserting: function(parent, html) + replaceBreaksToNewLines: function(html) { - var node = document.createElement('span'); - node.className = 'redactor-ie-paste'; - this.insert.node(node); + html = html.replace(/<br \/>\s*<br \/>/gi, "\n\n"); + html = html.replace(/<br\s?\/?>\n?<br\s?\/?>/gi, "\n<br /><br />"); - var parHtml = $(parent).html(); + html = html.replace(new RegExp("\r\n", 'g'), "\n"); + html = html.replace(new RegExp("\r", 'g'), "\n"); + html = html.replace(new RegExp("/\n\n+/"), 'g', "\n\n"); - parHtml = '<p>' + parHtml.replace(/<span class="redactor-ie-paste"><\/span>/gi, '</p>' + html + '<p>') + '</p>'; - $(parent).replaceWith(parHtml); + return html; }, - ie11PasteFrag: function(html) + clear: function(html) { - this.selection.get(); - this.range.deleteContents(); + html = html.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>'); + html = html.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>'); + html = html.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>'); + html = html.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>'); - var el = document.createElement("div"); - el.innerHTML = html; + html = html.replace(new RegExp('<p><p ', 'gi'), '<p '); + html = html.replace(new RegExp('<p><p>', 'gi'), '<p>'); + html = html.replace(new RegExp('</p></p>', 'gi'), '</p>'); + html = html.replace(new RegExp('<p>\\s?</p>', 'gi'), ''); + html = html.replace(new RegExp("\n</p>", 'gi'), '</p>'); + html = html.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>'); + html = html.replace(new RegExp('<p>\t*</p>', 'gi'), ''); - var frag = document.createDocumentFragment(), node, lastNode; - while ((node = el.firstChild)) - { - lastNode = frag.appendChild(node); - } - - this.range.insertNode(frag); + return html; } }; }, - caret: function() + paste: function() { return { - setStart: function(node) + init: function(e) { - // inline tag - if (!this.utils.isBlock(node)) - { - var space = this.utils.createSpaceElement(); + if (!this.opts.cleanOnPaste) return; - $(node).prepend(space); - this.caret.setEnd(space); - } - else - { - this.caret.set(node, 0, node, 0); - } - }, - setEnd: function(node) - { - this.caret.set(node, 1, node, 1); - }, - set: function(orgn, orgo, focn, foco) - { - // focus - if (!this.utils.browser('msie')) this.$editor.focus(); + this.rtePaste = true; - orgn = orgn[0] || orgn; - focn = focn[0] || focn; + this.buffer.set(); + this.selection.save(); + this.utils.saveScroll(); - if (this.utils.isBlockTag(orgn.tagName) && orgn.innerHTML === '') + this.paste.createPasteBox(); + + $(window).on('scroll.redactor-freeze', $.proxy(function() { - orgn.innerHTML = this.opts.invisibleSpace; - } + $(window).scrollTop(this.saveBodyScroll); - if (orgn.tagName == 'BR' && this.opts.linebreaks === false) + }, this)); + + setTimeout($.proxy(function() { - var par = $(this.opts.emptyHtml)[0]; - $(orgn).replaceWith(par); - orgn = par; - focn = orgn; - } + var html = this.$pasteBox.html(); - this.selection.get(); + this.$pasteBox.remove(); - try { - this.range.setStart(orgn, orgo); - this.range.setEnd(focn, foco); - } - catch (e) {} + this.selection.restore(); + this.utils.restoreScroll(); - this.selection.addRange(); - }, - setAfter: function(node) - { - var tag = $(node)[0].tagName; + this.paste.insert(html); - // inline tag - if (tag != 'BR' && !this.utils.isBlock(node)) - { - var space = this.utils.createSpaceElement(); + $(window).off('scroll.redactor-freeze'); - $(node).after(space); - this.caret.setEnd(space); - } - else - { - if (tag != 'BR' && this.utils.browser('msie')) - { - this.caret.setStart($(node).next()); - } - else - { - this.caret.setAfterOrBefore(node, 'after'); - } - } + }, this), 1); + }, - setBefore: function(node) + createPasteBox: function() { - // block tag - if (this.utils.isBlock(node)) - { - this.caret.setEnd($(node).prev()); - } - else - { - this.caret.setAfterOrBefore(node, 'before'); - } + this.$pasteBox = $('<div>').html('').attr('contenteditable', 'true').css({ position: 'fixed', width: 0, top: 0, left: '-9999px' }); + + this.$box.parent().append(this.$pasteBox); + this.$pasteBox.focus(); }, - setAfterOrBefore: function(node, type) + insert: function(html) { - // focus - if (!this.utils.browser('msie')) this.$editor.focus(); + html = this.core.setCallback('pasteBefore', html); - node = node[0] || node; + // clean + html = (this.utils.isSelectAll()) ? this.clean.onPaste(html, false) : this.clean.onPaste(html); - this.selection.get(); + html = this.core.setCallback('paste', html); - if (type == 'after') + if (this.utils.isSelectAll()) { - try { - - this.range.setStartAfter(node); - this.range.setEndAfter(node); - } - catch (e) {} + this.insert.set(html, false); } else { - try { - this.range.setStartBefore(node); - this.range.setEndBefore(node); - } - catch (e) {} + this.insert.html(html, false); } + this.utils.disableSelectAll(); + this.rtePaste = false; - this.range.collapse(false); - this.selection.addRange(); - }, - getOffsetOfElement: function(node) + setTimeout($.proxy(this.clean.clearUnverified, this), 10); + + // clean empty spans + setTimeout($.proxy(function() + { + var spans = this.$editor.find('span'); + $.each(spans, function(i,s) + { + var html = s.innerHTML.replace(/[\u200B-\u200D\uFEFF]/, ''); + if (html === '' && s.attributes.length === 0) $(s).remove(); + + }); + + }, this), 10); + } + }; + }, + placeholder: function() + { + return { + enable: function() { - node = node[0] || node; + if (!this.placeholder.is()) return; - this.selection.get(); + this.$editor.attr('placeholder', this.$element.attr('placeholder')); - var cloned = this.range.cloneRange(); - cloned.selectNodeContents(node); - cloned.setEnd(this.range.endContainer, this.range.endOffset); + this.placeholder.toggle(); + this.$editor.on('keyup.redactor-placeholder', $.proxy(this.placeholder.toggle, this)); - return $.trim(cloned.toString()).length; }, - getOffset: function() + toggle: function() { - var offset = 0; - var sel = window.getSelection(); - if (sel.rangeCount > 0) - { - var range = window.getSelection().getRangeAt(0); - var preCaretRange = range.cloneRange(); - preCaretRange.selectNodeContents(this.$editor[0]); - preCaretRange.setEnd(range.endContainer, range.endOffset); - offset = preCaretRange.toString().length; - } - - return offset; + var func = 'removeClass'; + if (this.utils.isEmpty(this.$editor.html(), false)) func = 'addClass'; + this.$editor[func]('redactor-placeholder'); }, - setOffset: function(start, end) + remove: function() { - if (typeof end == 'undefined') end = start; - if (!this.focus.isFocused()) this.focus.setStart(); - - var range = document.createRange(); - var sel = document.getSelection(); - var node, offset = 0; - var walker = document.createTreeWalker(this.$editor[0], NodeFilter.SHOW_TEXT, null, null); - - while (node = walker.nextNode()) + this.$editor.removeClass('redactor-placeholder'); + }, + is: function() + { + if (this.opts.placeholder) { - offset += node.nodeValue.length; - if (offset > start) - { - range.setStart(node, node.nodeValue.length + start - offset); - start = Infinity; - } - - if (offset >= end) - { - range.setEnd(node, node.nodeValue.length + end - offset); - break; - } + return this.$element.attr('placeholder', this.opts.placeholder); } - - sel.removeAllRanges(); - sel.addRange(range); - }, - setToPoint: function(start, end) + else + { + return !(typeof this.$element.attr('placeholder') == 'undefined' || this.$element.attr('placeholder') === ''); + } + } + }; + }, + progress: function() + { + return { + show: function() { - this.caret.setOffset(start, end); + $(document.body).append($('<div id="redactor-progress"><span></span></div>')); + $('#redactor-progress').fadeIn(); }, - getCoords: function() + hide: function() { - return this.caret.getOffset(); + $('#redactor-progress').fadeOut(1500, function() + { + $(this).remove(); + }); } + }; }, selection: function() { return { @@ -5937,1214 +6527,958 @@ return this.clean.onSync(html); } }; }, - observe: function() + shortcuts: function() { return { - load: function() + init: function(e, key) { - this.observe.images(); - this.observe.links(); - }, - buttons: function(e, btnName) - { - var current = this.selection.getCurrent(); - var parent = this.selection.getParent(); - - this.button.setInactiveAll(btnName); - - if (e === false && btnName !== 'html') + // disable browser's hot keys for bold and italic + if (!this.opts.shortcuts) { - if ($.inArray(btnName, this.opts.activeButtons) != -1) this.button.toggleActive(btnName); - return; + if ((e.ctrlKey || e.metaKey) && (key === 66 || key === 73)) e.preventDefault(); + return false; } - var linkButtonName = (this.utils.isCurrentOrParent('A')) ? this.lang.get('link_edit') : this.lang.get('link_insert'); - $('body').find('a.redactor-dropdown-link').text(linkButtonName); - - $.each(this.opts.activeButtonsStates, $.proxy(function(key, value) + $.each(this.opts.shortcuts, $.proxy(function(str, command) { - var parentEl = $(parent).closest(key); - var currentEl = $(current).closest(key); - - if (!this.utils.isRedactorParent(parentEl)) return; - if (!this.utils.isRedactorParent(currentEl)) return; - if (parentEl.length !== 0 || currentEl.closest(key).length !== 0) + var keys = str.split(','); + var len = keys.length; + for (var i = 0; i < len; i++) { + if (typeof keys[i] === 'string') + { + this.shortcuts.handler(e, $.trim(keys[i]), $.proxy(function() + { + var func; + if (command.func.search(/\./) != '-1') + { + func = command.func.split('.'); + if (typeof this[func[0]] != 'undefined') + { + this[func[0]][func[1]].apply(this, command.params); + } + } + else + { + this[command.func].apply(this, command.params); + } - this.button.setActive(value); + }, this)); + } + } }, this)); - - var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase()); - if (this.utils.isRedactorParent(parent) && $parent.length) - { - var align = ($parent.css('text-align') === '') ? 'left' : $parent.css('text-align'); - this.button.setActive('align' + align); - } }, - addButton: function(tag, btnName) + handler: function(e, keys, origHandler) { - this.opts.activeButtons.push(btnName); - this.opts.activeButtonsStates[tag] = btnName; - }, - images: function() - { - this.$editor.find('img').each($.proxy(function(i, img) + // based on https://github.com/jeresig/jquery.hotkeys + var hotkeysSpecialKeys = { - var $img = $(img); + 8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=", + 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'" + }; - // IE fix (when we clicked on an image and then press backspace IE does goes to image's url) - $img.closest('a').on('click', function(e) { e.preventDefault(); }); - if (this.utils.browser('msie')) $img.attr('unselectable', 'on'); + var hotkeysShiftNums = + { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + }; - this.image.setEditable($img); + keys = keys.toLowerCase().split(" "); + var special = hotkeysSpecialKeys[e.keyCode], + character = String.fromCharCode( e.which ).toLowerCase(), + modif = "", possible = {}; - }, this)); - - $(document).on('click.redactor-image-delete', $.proxy(function(e) + $.each([ "alt", "ctrl", "meta", "shift"], function(index, specialKey) { - this.observe.image = false; - if (e.target.tagName == 'IMG' && this.utils.isRedactorParent(e.target)) + if (e[specialKey + 'Key'] && special !== specialKey) { - this.observe.image = (this.observe.image && this.observe.image == e.target) ? false : e.target; + modif += specialKey + '+'; } + }); - }, this)); - }, - links: function() - { - if (!this.opts.linkTooltip) return; - - this.$editor.find('a').on('touchstart click', $.proxy(this.observe.showTooltip, this)); - this.$editor.on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this)); - $(document).on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this)); - }, - getTooltipPosition: function($link) - { - return $link.offset(); - }, - showTooltip: function(e) - { - var $link = $(e.target); - if ($link.size() === 0 || $link[0].tagName !== 'A') return; - - var pos = this.observe.getTooltipPosition($link); - - var tooltip = $('<span class="redactor-link-tooltip"></span>'); - - var href = $link.attr('href'); - if (href === undefined) + if (special) possible[modif + special] = true; + if (character) { - href = ''; + possible[modif + character] = true; + possible[modif + hotkeysShiftNums[character]] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if (modif === "shift+") + { + possible[hotkeysShiftNums[character]] = true; + } } - if (href.length > 24) href = href.substring(0, 24) + '...'; - - var aLink = $('<a href="' + $link.attr('href') + '" target="_blank" />').html(href).addClass('redactor-link-tooltip-action'); - var aEdit = $('<a href="#" />').html(this.lang.get('edit')).on('click', $.proxy(this.link.show, this)).addClass('redactor-link-tooltip-action'); - var aUnlink = $('<a href="#" />').html(this.lang.get('unlink')).on('click', $.proxy(this.link.unlink, this)).addClass('redactor-link-tooltip-action'); - - tooltip.append(aLink).append(' | ').append(aEdit).append(' | ').append(aUnlink); - tooltip.css({ - top: (pos.top + 20) + 'px', - left: pos.left + 'px' - }); - - $('.redactor-link-tooltip').remove(); - $('body').append(tooltip); - }, - closeTooltip: function(e) - { - e = e.originalEvent || e; - - if (e.target.tagName == 'A' && !$(e.target).hasClass('redactor-link-tooltip-action') && this.utils.isRedactorParent(e.target)) + for (var i = 0, len = keys.length; i < len; i++) { - return; + if (possible[keys[i]]) + { + e.preventDefault(); + return origHandler.apply(this, arguments); + } } - - $('.redactor-link-tooltip').remove(); } - }; }, - link: function() + tabifier: function() { return { - show: function(e) + get: function(code) { - if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); + if (!this.opts.tabifier) return code; - this.modal.load('link', this.lang.get('link_insert'), 600); + // clean setup + var ownLine = ['area', 'body', 'head', 'hr', 'i?frame', 'link', 'meta', 'noscript', 'style', 'script', 'table', 'tbody', 'thead', 'tfoot']; + var contOwnLine = ['li', 'dt', 'dt', 'h[1-6]', 'option', 'script']; + var newLevel = ['blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'p', 'pre', 'select', 'td', 'th', 'tr', 'ul']; - this.modal.createCancelButton(); - this.link.buttonInsert = this.modal.createActionButton(this.lang.get('insert')); + this.tabifier.lineBefore = new RegExp('^<(/?' + ownLine.join('|/?' ) + '|' + contOwnLine.join('|') + ')[ >]'); + this.tabifier.lineAfter = new RegExp('^<(br|/?' + ownLine.join('|/?' ) + '|/' + contOwnLine.join('|/') + ')[ >]'); + this.tabifier.newLevel = new RegExp('^</?(' + newLevel.join('|' ) + ')[ >]'); - this.selection.get(); + var i = 0, + codeLength = code.length, + point = 0, + start = null, + end = null, + tag = '', + out = '', + cont = ''; - this.link.getData(); - this.link.cleanUrl(); + this.tabifier.cleanlevel = 0; - if (this.link.target == '_blank') $('#redactor-link-blank').prop('checked', true); - - this.link.$inputUrl = $('#redactor-link-url'); - this.link.$inputText = $('#redactor-link-url-text'); - - this.link.$inputText.val(this.link.text); - this.link.$inputUrl.val(this.link.url); - - this.link.buttonInsert.on('click', $.proxy(this.link.insert, this)); - - // show modal - this.selection.save(); - this.modal.show(); - this.link.$inputUrl.focus(); - }, - cleanUrl: function() - { - var thref = self.location.href.replace(/\/$/i, ''); - this.link.url = this.link.url.replace(thref, ''); - this.link.url = this.link.url.replace(/^\/#/, '#'); - this.link.url = this.link.url.replace('mailto:', ''); - - // remove host from href - if (!this.opts.linkProtocol) + for (; i < codeLength; i++) { - var re = new RegExp('^(http|ftp|https)://' + self.location.host, 'i'); - this.link.url = this.link.url.replace(re, ''); - } + point = i; - }, - getData: function() - { - this.link.$node = false; + // if no more tags, copy and exit + if (-1 == code.substr(i).indexOf( '<' )) + { + out += code.substr(i); - var $el = $(this.selection.getCurrent()).closest('a'); - if ($el.size() !== 0 && $el[0].tagName === 'A') - { - this.link.$node = $el; + return this.tabifier.finish(out); + } - this.link.url = $el.attr('href'); - this.link.text = $el.text(); - this.link.target = $el.attr('target'); - } - else - { - this.link.text = this.sel.toString(); - this.link.url = ''; - this.link.target = ''; - } + // copy verbatim until a tag + while (point < codeLength && code.charAt(point) != '<') + { + point++; + } - }, - insert: function() - { - var target = ''; - var link = this.link.$inputUrl.val(); - var text = this.link.$inputText.val(); - - if ($.trim(link) === '') - { - this.link.$inputUrl.addClass('redactor-input-error').on('keyup', function() + if (i != point) { - $(this).removeClass('redactor-input-error'); - $(this).off('keyup'); + cont = code.substr(i, point - i); + if (!cont.match(/^\s{2,}$/g)) + { + if ('\n' == out.charAt(out.length - 1)) out += this.tabifier.getTabs(); + else if ('\n' == cont.charAt(0)) + { + out += '\n' + this.tabifier.getTabs(); + cont = cont.replace(/^\s+/, ''); + } - }); - return; - } + out += cont; + } - // mailto - if (link.search('@') != -1 && /(http|ftp|https):\/\//i.test(link) === false) - { - link = 'mailto:' + link; - } - // url, not anchor - else if (link.search('#') !== 0) - { - if ($('#redactor-link-blank').prop('checked')) - { - target = '_blank'; + if (cont.match(/\n/)) out += '\n' + this.tabifier.getTabs(); } - // test url (add protocol) - var pattern = '((xn--)?[a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}'; - var re = new RegExp('^(http|ftp|https)://' + pattern, 'i'); - var re2 = new RegExp('^' + pattern, 'i'); + start = point; - if (link.search(re) == -1 && link.search(re2) === 0 && this.opts.linkProtocol) + // find the end of the tag + while (point < codeLength && '>' != code.charAt(point)) { - link = this.opts.linkProtocol + '://' + link; + point++; } - } - this.link.set(text, link, target); - this.modal.close(); - }, - set: function(text, link, target) - { - text = $.trim(text.replace(/<|>/g, '')); + tag = code.substr(start, point - start); + i = point; - this.selection.restore(); + var t; - if (text === '' && link === '') return; - if (text === '' && link !== '') text = link; + if ('!--' == tag.substr(1, 3)) + { + if (!tag.match(/--$/)) + { + while ('-->' != code.substr(point, 3)) + { + point++; + } + point += 2; + tag = code.substr(start, point - start); + i = point; + } - if (this.link.$node) - { - this.buffer.set(); + if ('\n' != out.charAt(out.length - 1)) out += '\n'; - this.link.$node.text(text).attr('href', link); - if (target !== '') - { - this.link.$node.attr('target', target); + out += this.tabifier.getTabs(); + out += tag + '>\n'; } - else + else if ('!' == tag[1]) { - this.link.$node.removeAttr('target'); + out = this.tabifier.placeTag(tag + '>', out); } - - this.code.sync(); - } - else - { - if (this.utils.browser('mozilla') && this.link.text === '') + else if ('?' == tag[1]) { - var $a = $('<a />').attr('href', link).text(text); - if (target !== '') $a.attr('target', target); - - this.insert.node($a); - this.selection.selectElement($a); + out += tag + '>\n'; } - else + else if (t = tag.match(/^<(script|style|pre)/i)) { - document.execCommand('createLink', false, link); + t[1] = t[1].toLowerCase(); + tag = this.tabifier.cleanTag(tag); + out = this.tabifier.placeTag(tag, out); + end = String(code.substr(i + 1)).toLowerCase().indexOf('</' + t[1]); - var $a = $(this.selection.getCurrent()).closest('a'); - - if (target !== '') $a.attr('target', target); - $a.removeAttr('style'); - - if (this.link.text === '') + if (end) { - $a.text(text); - this.selection.selectElement($a); + cont = code.substr(i + 1, end); + i += end; + out += cont; } } - - this.code.sync(); - this.core.setCallback('insertedLink', $a); - + else + { + tag = this.tabifier.cleanTag(tag); + out = this.tabifier.placeTag(tag, out); + } } - // link tooltip - setTimeout($.proxy(function() - { - this.observe.links(); - - }, this), 5); + return this.tabifier.finish(out); }, - unlink: function(e) + getTabs: function() { - if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); - - var nodes = this.selection.getNodes(); - if (!nodes) return; - - this.buffer.set(); - - var len = nodes.length; - for (var i = 0; i < len; i++) + var s = ''; + for ( var j = 0; j < this.tabifier.cleanlevel; j++ ) { - if (nodes[i].tagName == 'A') - { - var $node = $(nodes[i]); - $node.replaceWith($node.contents()); - } + s += '\t'; } - // remove tooltip - $('.redactor-link-tooltip').remove(); - - this.code.sync(); - - } - }; - }, - image: function() - { - return { - show: function() + return s; + }, + finish: function(code) { - this.modal.load('image', this.lang.get('image'), 700); - this.upload.init('#redactor-modal-image-droparea', this.opts.imageUpload, this.image.insert); + code = code.replace(/\n\s*\n/g, '\n'); + code = code.replace(/^[\s\n]*/, ''); + code = code.replace(/[\s\n]*$/, ''); + code = code.replace(/<script(.*?)>\n<\/script>/gi, '<script$1></script>'); - this.selection.save(); - this.modal.show(); + this.tabifier.cleanlevel = 0; + return code; }, - showEdit: function($image) + cleanTag: function (tag) { - var $link = $image.closest('a'); + var tagout = ''; + tag = tag.replace(/\n/g, ' '); + tag = tag.replace(/\s{2,}/g, ' '); + tag = tag.replace(/^\s+|\s+$/g, ' '); - this.modal.load('imageEdit', this.lang.get('edit'), 705); - - this.modal.createCancelButton(); - this.image.buttonDelete = this.modal.createDeleteButton(this.lang.get('_delete')); - this.image.buttonSave = this.modal.createActionButton(this.lang.get('save')); - - this.image.buttonDelete.on('click', $.proxy(function() + var suffix = ''; + if (tag.match(/\/$/)) { - this.image.remove($image); + suffix = '/'; + tag = tag.replace(/\/+$/, ''); + } - }, this)); - - this.image.buttonSave.on('click', $.proxy(function() + var m; + while (m = /\s*([^= ]+)(?:=((['"']).*?\3|[^ ]+))?/.exec(tag)) { - this.image.update($image); + if (m[2]) tagout += m[1].toLowerCase() + '=' + m[2]; + else if (m[1]) tagout += m[1].toLowerCase(); - }, this)); - - - $('#redactor-image-title').val($image.attr('alt')); - - if (!this.opts.imageLink) $('.redactor-image-link-option').hide(); - else - { - var $redactorImageLink = $('#redactor-image-link'); - - $redactorImageLink.attr('href', $image.attr('src')); - if ($link.size() !== 0) - { - $redactorImageLink.val($link.attr('href')); - if ($link.attr('target') == '_blank') $('#redactor-image-link-blank').prop('checked', true); - } + tagout += ' '; + tag = tag.substr(m[0].length); } - if (!this.opts.imagePosition) $('.redactor-image-position-option').hide(); - else + return tagout.replace(/\s*$/, '') + suffix + '>'; + }, + placeTag: function (tag, out) + { + var nl = tag.match(this.tabifier.newLevel); + if (tag.match(this.tabifier.lineBefore) || nl) { - var floatValue = ($image.css('display') == 'block' && $image.css('float') == 'none') ? 'center' : $image.css('float'); - $('#redactor-image-align').val(floatValue); + out = out.replace(/\s*$/, ''); + out += '\n'; } - this.modal.show(); + if (nl && '/' == tag.charAt(1)) this.tabifier.cleanlevel--; + if ('\n' == out.charAt(out.length - 1)) out += this.tabifier.getTabs(); + if (nl && '/' != tag.charAt(1)) this.tabifier.cleanlevel++; - }, - setFloating: function($image) - { - var floating = $('#redactor-image-align').val(); + out += tag; - var imageFloat = ''; - var imageDisplay = ''; - var imageMargin = ''; - - switch (floating) + if (tag.match(this.tabifier.lineAfter) || tag.match(this.tabifier.newLevel)) { - case 'left': - imageFloat = 'left'; - imageMargin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0'; - break; - case 'right': - imageFloat = 'right'; - imageMargin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin; - break; - case 'center': - imageDisplay = 'block'; - imageMargin = 'auto'; - break; + out = out.replace(/ *$/, ''); + out += '\n'; } - $image.css({ 'float': imageFloat, display: imageDisplay, margin: imageMargin }); - $image.attr('rel', $image.attr('style')); - }, - update: function($image) + return out; + } + }; + }, + tidy: function() + { + return { + setupAllowed: function() { - this.image.hideResize(); - this.buffer.set(); + if (this.opts.allowedTags) this.opts.deniedTags = false; + if (this.opts.allowedAttr) this.opts.removeAttr = false; - var $link = $image.closest('a'); + if (this.opts.linebreaks) return; - $image.attr('alt', $('#redactor-image-title').val()); + var tags = ['p', 'section']; + if (this.opts.allowedTags) this.tidy.addToAllowed(tags); + if (this.opts.deniedTags) this.tidy.removeFromDenied(tags); - this.image.setFloating($image); - - // as link - var link = $.trim($('#redactor-image-link').val()); - if (link !== '') + }, + addToAllowed: function(tags) + { + var len = tags.length; + for (var i = 0; i < len; i++) { - var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false; - - if ($link.size() === 0) + if ($.inArray(tags[i], this.opts.allowedTags) == -1) { - var a = $('<a href="' + link + '">' + this.utils.getOuterHtml($image) + '</a>'); - if (target) a.attr('target', '_blank'); - - $image.replaceWith(a); + this.opts.allowedTags.push(tags[i]); } - else - { - $link.attr('href', link); - if (target) - { - $link.attr('target', '_blank'); - } - else - { - $link.removeAttr('target'); - } - } } - else if ($link.size() !== 0) - { - $link.replaceWith(this.utils.getOuterHtml($image)); - - } - - this.modal.close(); - this.observe.images(); - this.code.sync(); - - }, - setEditable: function($image) + removeFromDenied: function(tags) { - if (!this.opts.imageEditable) return; - - $image.on('dragstart', $.proxy(this.image.onDrag, this)); - - $image.on('mousedown', $.proxy(this.image.hideResize, this)); - $image.on('click touchstart', $.proxy(function(e) + var len = tags.length; + for (var i = 0; i < len; i++) { - this.observe.image = $image; - - if (this.$editor.find('#redactor-image-box').size() !== 0) return false; - - - this.image.resizer = this.image.loadEditableControls($image); - - $(document).on('click.redactor-image-resize-hide', $.proxy(this.image.hideResize, this)); - this.$editor.on('click.redactor-image-resize-hide', $.proxy(this.image.hideResize, this)); - - // resize - if (!this.opts.imageResizable) return; - - this.image.resizer.on('mousedown.redactor touchstart.redactor', $.proxy(function(e) + var pos = $.inArray(tags[i], this.opts.deniedTags); + if (pos != -1) { - e.preventDefault(); - - this.image.resizeHandle = { - x : e.pageX, - y : e.pageY, - el : $image, - ratio: $image.width() / $image.height(), - h: $image.height() - }; - - e = e.originalEvent || e; - - if (e.targetTouches) - { - this.image.resizeHandle.x = e.targetTouches[0].pageX; - this.image.resizeHandle.y = e.targetTouches[0].pageY; - } - - this.image.startResize(); - - }, this)); - - - }, this)); + this.opts.deniedTags.splice(pos, 1); + } + } }, - startResize: function() + load: function(html, options) { - $(document).on('mousemove.redactor-image-resize touchmove.redactor-image-resize', $.proxy(this.image.moveResize, this)); - $(document).on('mouseup.redactor-image-resize touchend.redactor-image-resize', $.proxy(this.image.stopResize, this)); - }, - moveResize: function(e) - { - e.preventDefault(); + this.tidy.settings = { + deniedTags: this.opts.deniedTags, + allowedTags: this.opts.allowedTags, + removeComments: this.opts.removeComments, + replaceTags: this.opts.replaceTags, + replaceStyles: this.opts.replaceStyles, + removeDataAttr: this.opts.removeDataAttr, + removeAttr: this.opts.removeAttr, + allowedAttr: this.opts.allowedAttr, + removeWithoutAttr: this.opts.removeWithoutAttr, + removeEmpty: this.opts.removeEmpty + }; - e = e.originalEvent || e; + $.extend(this.tidy.settings, options); - var height = this.image.resizeHandle.h; + html = this.tidy.removeComments(html); - if (e.targetTouches) height += (e.targetTouches[0].pageY - this.image.resizeHandle.y); - else height += (e.pageY - this.image.resizeHandle.y); + // create container + this.tidy.$div = $('<div />').append(html); + // clean + this.tidy.replaceTags(); + this.tidy.replaceStyles(); + this.tidy.removeTags(); - var width = Math.round(height * this.image.resizeHandle.ratio); + this.tidy.removeAttr(); + this.tidy.removeEmpty(); + this.tidy.removeParagraphsInLists(); + this.tidy.removeDataAttr(); + this.tidy.removeWithoutAttr(); - if (height < 50 || width < 100) return; + html = this.tidy.$div.html(); + this.tidy.$div.remove(); - this.image.resizeHandle.el.height(height); - this.image.resizeHandle.el.width(width); - - this.code.sync(); + return html; }, - stopResize: function() + removeComments: function(html) { - this.handle = false; - $(document).off('.redactor-image-resize'); - this.image.hideResize(); + if (!this.tidy.settings.removeComments) return html; + + return html.replace(/<!--[\s\S]*?-->/gi, ''); }, - onDrag: function(e) + replaceTags: function(html) { - if (this.$editor.find('#redactor-image-box').size() !== 0) + if (!this.tidy.settings.replaceTags) return html; + + var len = this.tidy.settings.replaceTags.length; + var replacement = [], rTags = []; + for (var i = 0; i < len; i++) { - e.preventDefault(); - return false; + rTags.push(this.tidy.settings.replaceTags[i][1]); + replacement.push(this.tidy.settings.replaceTags[i][0]); } - this.$editor.on('drop.redactor-image-inside-drop', $.proxy(function() + this.tidy.$div.find(replacement.join(',')).each($.proxy(function(n,s) { - setTimeout($.proxy(this.image.onDrop, this), 1); + var tag = rTags[n]; + $(s).replaceWith(function() + { + var replaced = $('<' + tag + ' />').append($(this).contents()); + for (var i = 0; i < this.attributes.length; i++) + { + replaced.attr(this.attributes[i].name, this.attributes[i].value); + } + + return replaced; + }); + }, this)); + + return html; }, - onDrop: function() + replaceStyles: function() { - this.image.fixImageSourceAfterDrop(); - this.observe.images(); - this.$editor.off('drop.redactor-image-inside-drop'); - this.clean.clearUnverified(); - this.code.sync(); - }, - fixImageSourceAfterDrop: function() - { - this.$editor.find('img[data-save-url]').each(function() - { - var $el = $(this); - $el.attr('src', $el.attr('data-save-url')); - $el.removeAttr('data-save-url'); - }); - }, - hideResize: function(e) - { - if (e && $(e.target).closest('#redactor-image-box').length !== 0) return; - if (e && e.target.tagName == 'IMG') - { - var $image = $(e.target); - $image.attr('data-save-url', $image.attr('src')); - } + if (!this.tidy.settings.replaceStyles) return; - var imageBox = this.$editor.find('#redactor-image-box'); - if (imageBox.size() === 0) return; - - this.image.editter.remove(); - $(this.image.resizer).remove(); - - imageBox.find('img').css({ - marginTop: imageBox[0].style.marginTop, - marginBottom: imageBox[0].style.marginBottom, - marginLeft: imageBox[0].style.marginLeft, - marginRight: imageBox[0].style.marginRight - }); - - imageBox.css('margin', ''); - imageBox.find('img').css('opacity', ''); - imageBox.replaceWith(function() + var len = this.tidy.settings.replaceStyles.length; + this.tidy.$div.find('span').each($.proxy(function(n,s) { - return $(this).contents(); - }); + var $el = $(s); + var style = $el.attr('style'); + for (var i = 0; i < len; i++) + { + if (style && style.match(new RegExp('^' + this.tidy.settings.replaceStyles[i][0], 'i'))) + { + var tagName = this.tidy.settings.replaceStyles[i][1]; + $el.replaceWith(function() + { + var tag = document.createElement(tagName); + return $(tag).append($(this).contents()); + }); + } + } - $(document).off('click.redactor-image-resize-hide'); - this.$editor.off('click.redactor-image-resize-hide'); + }, this)); - this.code.sync(); - }, - loadEditableControls: function($image) + removeTags: function() { - var imageBox = $('<span id="redactor-image-box" data-redactor="verified">'); - imageBox.css('float', $image.css('float')).attr('contenteditable', false); - - if ($image[0].style.margin != 'auto') + if (!this.tidy.settings.deniedTags && this.tidy.settings.allowedTags) { - imageBox.css({ - marginTop: $image[0].style.marginTop, - marginBottom: $image[0].style.marginBottom, - marginLeft: $image[0].style.marginLeft, - marginRight: $image[0].style.marginRight + this.tidy.$div.find('*').not(this.tidy.settings.allowedTags.join(',')).each(function(i, s) + { + if (s.innerHTML === '') $(s).remove(); + else $(s).contents().unwrap(); }); - - $image.css('margin', ''); } - else + + if (this.tidy.settings.deniedTags) { - imageBox.css({ 'display': 'block', 'margin': 'auto' }); + this.tidy.$div.find(this.tidy.settings.deniedTags.join(',')).each(function(i, s) + { + if (s.innerHTML === '') $(s).remove(); + else $(s).contents().unwrap(); + }); } - - $image.css('opacity', '.5').after(imageBox); - - // editter - this.image.editter = $('<span id="redactor-image-editter" data-redactor="verified">' + this.lang.get('edit') + '</span>'); - this.image.editter.attr('contenteditable', false); - this.image.editter.on('click', $.proxy(function() + }, + removeAttr: function() + { + var len; + if (!this.tidy.settings.removeAttr && this.tidy.settings.allowedAttr) { - this.image.showEdit($image); - }, this)); - imageBox.append(this.image.editter); + var allowedAttrTags = [], allowedAttrData = []; + len = this.tidy.settings.allowedAttr.length; + for (var i = 0; i < len; i++) + { + allowedAttrTags.push(this.tidy.settings.allowedAttr[i][0]); + allowedAttrData.push(this.tidy.settings.allowedAttr[i][1]); + } - // position correction - var editerWidth = this.image.editter.innerWidth(); - this.image.editter.css('margin-left', '-' + editerWidth/2 + 'px'); + this.tidy.$div.find('*').each($.proxy(function(n,s) + { + var $el = $(s); + var pos = $.inArray($el[0].tagName.toLowerCase(), allowedAttrTags); + var attributesRemove = this.tidy.removeAttrGetRemoves(pos, allowedAttrData, $el); - // resizer - if (this.opts.imageResizable && !this.utils.isMobile()) - { - var imageResizer = $('<span id="redactor-image-resizer" data-redactor="verified"></span>'); + if (attributesRemove) + { + $.each(attributesRemove, function(z,f) { + $el.removeAttr(f); + }); + } + }, this)); + } - if (!this.utils.isDesktop()) + if (this.tidy.settings.removeAttr) + { + len = this.tidy.settings.removeAttr.length; + for (var i = 0; i < len; i++) { - imageResizer.css({ width: '15px', height: '15px' }); - } + var attrs = this.tidy.settings.removeAttr[i][1]; + if ($.isArray(attrs)) attrs = attrs.join(' '); - imageResizer.attr('contenteditable', false); - imageBox.append(imageResizer); - imageBox.append($image); - - return imageResizer; + this.tidy.$div.find(this.tidy.settings.removeAttr[i][0]).removeAttr(attrs); + } } - else - { - imageBox.append($image); - return false; - } + }, - remove: function(image) + removeAttrGetRemoves: function(pos, allowed, $el) { - var $image = $(image); - var $link = $image.closest('a'); - var $figure = $image.closest('figure'); - var $parent = $image.parent(); - if ($('#redactor-image-box').size() !== 0) - { - $parent = $('#redactor-image-box').parent(); - } + var attributesRemove = []; - var $next; - if ($figure.size() !== 0) + // remove all attrs + if (pos == -1) { - $next = $figure.next(); - $figure.remove(); + $.each($el[0].attributes, function(i, item) + { + attributesRemove.push(item.name); + }); + } - else if ($link.size() !== 0) + // allow all attrs + else if (allowed[pos] == '*') { - $parent = $link.parent(); - $link.remove(); + attributesRemove = []; } + // allow specific attrs else { - $image.remove(); - } + $.each($el[0].attributes, function(i, item) + { + if ($.isArray(allowed[pos])) + { + if ($.inArray(item.name, allowed[pos]) == -1) + { + attributesRemove.push(item.name); + } + } + else if (allowed[pos] != item.name) + { + attributesRemove.push(item.name); + } - $('#redactor-image-box').remove(); - - if ($figure.size() !== 0) - { - this.caret.setStart($next); + }); } - else - { - this.caret.setStart($parent); - } - // delete callback - this.core.setCallback('imageDelete', $image[0].src, $image); - - this.modal.close(); - this.code.sync(); + return attributesRemove; }, - insert: function(json, direct, e) + removeAttrs: function (el, regex) { - // error callback - if (typeof json.error != 'undefined') + regex = new RegExp(regex, "g"); + return el.each(function() { - this.modal.close(); - this.selection.restore(); - this.core.setCallback('imageUploadError', json); - return; - } + var self = $(this); + var len = this.attributes.length - 1; + for (var i = len; i >= 0; i--) + { + var item = this.attributes[i]; + if (item && item.specified && item.name.search(regex)>=0) + { + self.removeAttr(item.name); + } + } + }); + }, + removeEmpty: function() + { + if (!this.tidy.settings.removeEmpty) return; - var $img; - if (typeof json == 'string') + this.tidy.$div.find(this.tidy.settings.removeEmpty.join(',')).each(function() { - $img = $(json).attr('data-redactor-inserted-image', 'true'); - } - else - { - $img = $('<img>'); - $img.attr('src', json.filelink).attr('data-redactor-inserted-image', 'true'); - } + var $el = $(this); + var text = $el.text(); + text = text.replace(/[\u200B-\u200D\uFEFF]/g, ''); + text = text.replace(/&nbsp;/gi, ''); + text = text.replace(/\s/g, ''); + if (text === '' && $el.children().length === 0) + { + $el.remove(); + } + }); + }, + removeParagraphsInLists: function() + { + this.tidy.$div.find('li p').contents().unwrap(); + }, + removeDataAttr: function() + { + if (!this.tidy.settings.removeDataAttr) return; - var node = $img; - var isP = this.utils.isCurrentOrParent('P'); - if (isP) - { - // will replace - node = $('<blockquote />').append($img); - } + var tags = this.tidy.settings.removeDataAttr; + if ($.isArray(this.tidy.settings.removeDataAttr)) tags = this.tidy.settings.removeDataAttr.join(','); - if (direct) - { - this.selection.removeMarkers(); - var marker = this.selection.getMarker(); - this.insert.nodeToCaretPositionFromPoint(e, marker); - } - else - { - this.modal.close(); - } + this.tidy.removeAttrs(this.tidy.$div.find(tags), '^(data-)'); - this.selection.restore(); - this.buffer.set(); + }, + removeWithoutAttr: function() + { + if (!this.tidy.settings.removeWithoutAttr) return; - - this.insert.html(this.utils.getOuterHtml(node), false); - - var $image = this.$editor.find('img[data-redactor-inserted-image=true]').removeAttr('data-redactor-inserted-image'); - - if (isP) + this.tidy.$div.find(this.tidy.settings.removeWithoutAttr.join(',')).each(function() { - $image.parent().contents().unwrap().wrap('<p />'); - } - else if (this.opts.linebreaks) - { - $image.before('<br>').after('<br>'); - } - - if (typeof json == 'string') return; - - this.core.setCallback('imageUpload', $image, json); - + if (this.attributes.length === 0) + { + $(this).contents().unwrap(); + } + }); } }; }, - file: function() + toolbar: function() { return { - show: function() + init: function() { - this.modal.load('file', this.lang.get('file'), 700); - this.upload.init('#redactor-modal-file-upload', this.opts.fileUpload, this.file.insert); - - this.selection.save(); - - this.selection.get(); - var text = this.sel.toString(); - - $('#redactor-filename').val(text); - - this.modal.show(); + return { + html: + { + title: this.lang.get('html'), + func: 'code.toggle' + }, + formatting: + { + title: this.lang.get('formatting'), + dropdown: + { + p: + { + title: this.lang.get('paragraph'), + func: 'block.format' + }, + blockquote: + { + title: this.lang.get('quote'), + func: 'block.format' + }, + pre: + { + title: this.lang.get('code'), + func: 'block.format' + }, + h1: + { + title: this.lang.get('header1'), + func: 'block.format' + }, + h2: + { + title: this.lang.get('header2'), + func: 'block.format' + }, + h3: + { + title: this.lang.get('header3'), + func: 'block.format' + }, + h4: + { + title: this.lang.get('header4'), + func: 'block.format' + }, + h5: + { + title: this.lang.get('header5'), + func: 'block.format' + } + } + }, + bold: + { + title: this.lang.get('bold'), + func: 'inline.format' + }, + italic: + { + title: this.lang.get('italic'), + func: 'inline.format' + }, + deleted: + { + title: this.lang.get('deleted'), + func: 'inline.format' + }, + underline: + { + title: this.lang.get('underline'), + func: 'inline.format' + }, + unorderedlist: + { + title: '&bull; ' + this.lang.get('unorderedlist'), + func: 'list.toggle' + }, + orderedlist: + { + title: '1. ' + this.lang.get('orderedlist'), + func: 'list.toggle' + }, + outdent: + { + title: '< ' + this.lang.get('outdent'), + func: 'indent.decrease' + }, + indent: + { + title: '> ' + this.lang.get('indent'), + func: 'indent.increase' + }, + image: + { + title: this.lang.get('image'), + func: 'image.show' + }, + file: + { + title: this.lang.get('file'), + func: 'file.show' + }, + link: + { + title: this.lang.get('link'), + dropdown: + { + link: + { + title: this.lang.get('link_insert'), + func: 'link.show' + }, + unlink: + { + title: this.lang.get('unlink'), + func: 'link.unlink' + } + } + }, + alignment: + { + title: this.lang.get('alignment'), + dropdown: + { + left: + { + title: this.lang.get('align_left'), + func: 'alignment.left' + }, + center: + { + title: this.lang.get('align_center'), + func: 'alignment.center' + }, + right: + { + title: this.lang.get('align_right'), + func: 'alignment.right' + }, + justify: + { + title: this.lang.get('align_justify'), + func: 'alignment.justify' + } + } + }, + horizontalrule: + { + title: this.lang.get('horizontalrule'), + func: 'line.insert' + } + }; }, - insert: function(json, direct, e) + build: function() { - // error callback - if (typeof json.error != 'undefined') - { - this.modal.close(); - this.selection.restore(); - this.core.setCallback('fileUploadError', json); - return; - } + this.toolbar.hideButtons(); + this.toolbar.hideButtonsOnMobile(); + this.toolbar.isButtonSourceNeeded(); - var link; - if (typeof json == 'string') - { - link = json; - } - else - { - var text = $('#redactor-filename').val(); - if (typeof text == 'undefined' || text === '') text = json.filename; + if (this.opts.buttons.length === 0) return; - link = '<a href="' + json.filelink + '" id="filelink-marker">' + text + '</a>'; - } + this.$toolbar = this.toolbar.createContainer(); - if (direct) + this.toolbar.setOverflow(); + this.toolbar.append(); + this.toolbar.setFormattingTags(); + this.toolbar.loadButtons(); + this.toolbar.setFixed(); + + // buttons response + if (this.opts.activeButtons) { - this.selection.removeMarkers(); - var marker = this.selection.getMarker(); - this.insert.nodeToCaretPositionFromPoint(e, marker); + this.$editor.on('mouseup.redactor keyup.redactor focus.redactor', $.proxy(this.observe.buttons, this)); } - else - { - this.modal.close(); - } - this.selection.restore(); - this.buffer.set(); - - - this.insert.html(link); - - if (typeof json == 'string') return; - - var linkmarker = $(this.$editor.find('a#filelink-marker')); - if (linkmarker.size() !== 0) linkmarker.removeAttr('id'); - else linkmarker = false; - - - this.core.setCallback('fileUpload', linkmarker, json); - - } - }; - }, - modal: function() - { - return { - callbacks: {}, - loadTemplates: function() - { - this.opts.modal = { - imageEdit: String() - + '<section id="redactor-modal-image-edit">' - + '<label>' + this.lang.get('title') + '</label>' - + '<input type="text" id="redactor-image-title" />' - + '<label class="redactor-image-link-option">' + this.lang.get('link') + '</label>' - + '<input type="text" id="redactor-image-link" class="redactor-image-link-option" />' - + '<label class="redactor-image-link-option"><input type="checkbox" id="redactor-image-link-blank"> ' + this.lang.get('link_new_tab') + '</label>' - + '<label class="redactor-image-position-option">' + this.lang.get('image_position') + '</label>' - + '<select class="redactor-image-position-option" id="redactor-image-align">' - + '<option value="none">' + this.lang.get('none') + '</option>' - + '<option value="left">' + this.lang.get('left') + '</option>' - + '<option value="center">' + this.lang.get('center') + '</option>' - + '<option value="right">' + this.lang.get('right') + '</option>' - + '</select>' - + '</section>', - - image: String() - + '<section id="redactor-modal-image-insert">' - + '<div id="redactor-modal-image-droparea"></div>' - + '</section>', - - file: String() - + '<section id="redactor-modal-file-insert">' - + '<div id="redactor-modal-file-upload-box">' - + '<label>' + this.lang.get('filename') + '</label>' - + '<input type="text" id="redactor-filename" /><br><br>' - + '<div id="redactor-modal-file-upload"></div>' - + '</div>' - + '</section>', - - link: String() - + '<section id="redactor-modal-link-insert">' - + '<label>URL</label>' - + '<input type="url" id="redactor-link-url" />' - + '<label>' + this.lang.get('text') + '</label>' - + '<input type="text" id="redactor-link-url-text" />' - + '<label><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get('link_new_tab') + '</label>' - + '</section>' - }; - - - $.extend(this.opts, this.opts.modal); - }, - addCallback: function(name, callback) + createContainer: function() { - this.modal.callbacks[name] = callback; + return $('<ul>').addClass('redactor-toolbar').attr('id', 'redactor-toolbar-' + this.uuid); }, - createTabber: function($modal) + setFormattingTags: function() { - this.modal.$tabber = $('<div>').attr('id', 'redactor-modal-tabber'); + $.each(this.opts.toolbar.formatting.dropdown, $.proxy(function (i, s) + { + if ($.inArray(i, this.opts.formatting) == -1) delete this.opts.toolbar.formatting.dropdown[i]; + }, this)); - $modal.prepend(this.modal.$tabber); }, - addTab: function(id, name, active) + loadButtons: function() { - var $tab = $('<a href="#" rel="tab' + id + '">').text(name); - if (active) + $.each(this.opts.buttons, $.proxy(function(i, btnName) { - $tab.addClass('active'); - } + if (!this.opts.toolbar[btnName]) return; - var self = this; - $tab.on('click', function(e) - { - e.preventDefault(); - $('.redactor-tab').hide(); - $('.redactor-' + $(this).attr('rel')).show(); + if (this.opts.fileUpload === false && btnName === 'file') return true; + if ((this.opts.imageUpload === false && this.opts.s3 === false) && btnName === 'image') return true; - self.modal.$tabber.find('a').removeClass('active'); - $(this).addClass('active'); + var btnObject = this.opts.toolbar[btnName]; + this.$toolbar.append($('<li>').append(this.button.build(btnName, btnObject))); - }); - - this.modal.$tabber.append($tab); + }, this)); }, - addTemplate: function(name, template) + append: function() { - this.opts.modal[name] = template; + if (this.opts.toolbarExternal) + { + this.$toolbar.addClass('redactor-toolbar-external'); + $(this.opts.toolbarExternal).html(this.$toolbar); + } + else + { + this.$box.prepend(this.$toolbar); + } }, - getTemplate: function(name) + setFixed: function() { - return this.opts.modal[name]; + if (!this.utils.isDesktop()) return; + if (this.opts.toolbarExternal) return; + if (!this.opts.toolbarFixed) return; + + this.toolbar.observeScroll(); + $(this.opts.toolbarFixedTarget).on('scroll.redactor', $.proxy(this.toolbar.observeScroll, this)); + }, - getModal: function() + setOverflow: function() { - return this.$modalBody.find('section'); + if (this.utils.isMobile() && this.opts.toolbarOverflow) + { + this.$toolbar.addClass('redactor-toolbar-overflow'); + } }, - load: function(templateName, title, width) + isButtonSourceNeeded: function() { - this.modal.templateName = templateName; - this.modal.width = width; + if (this.opts.buttonSource) return; - this.modal.build(); - this.modal.enableEvents(); - this.modal.setTitle(title); - this.modal.setDraggable(); - this.modal.setContent(); - - // callbacks - if (typeof this.modal.callbacks[templateName] != 'undefined') + var index = this.opts.buttons.indexOf('html'); + if (index !== -1) { - this.modal.callbacks[templateName].call(this); + this.opts.buttons.splice(index, 1); } - }, - show: function() + hideButtons: function() { - this.modal.bodyOveflow = $(document.body).css('overflow'); - $(document.body).css('overflow', 'hidden'); + if (this.opts.buttonsHide.length === 0) return; - if (this.utils.isMobile()) + $.each(this.opts.buttonsHide, $.proxy(function(i, s) { - this.modal.showOnMobile(); - } - else - { - this.modal.showOnDesktop(); - } + var index = this.opts.buttons.indexOf(s); + this.opts.buttons.splice(index, 1); - this.$modalOverlay.show(); - this.$modalBox.show(); + }, this)); + }, + hideButtonsOnMobile: function() + { + if (!this.utils.isMobile() || this.opts.buttonsHideOnMobile.length === 0) return; - this.modal.setButtonsWidth(); - - this.utils.saveScroll(); - - // resize - if (!this.utils.isMobile()) + $.each(this.opts.buttonsHideOnMobile, $.proxy(function(i, s) { - setTimeout($.proxy(this.modal.showOnDesktop, this), 0); - $(window).on('resize.redactor-modal', $.proxy(this.modal.resize, this)); - } + var index = this.opts.buttons.indexOf(s); + this.opts.buttons.splice(index, 1); - // modal shown callback - this.core.setCallback('modalOpened', this.modal.templateName, this.$modal); - - // fix bootstrap modal focus - $(document).off('focusin.modal'); - - // enter - this.$modal.find('input[type=text]').on('keypress.redactor-modal', $.proxy(this.modal.setEnter, this)); - + }, this)); }, - showOnDesktop: function() + observeScroll: function() { - var height = this.$modal.outerHeight(); - var windowHeight = $(window).height(); - var windowWidth = $(window).width(); + var scrollTop = $(this.opts.toolbarFixedTarget).scrollTop(); + var boxTop = 1; - if (this.modal.width > windowWidth) + if (this.opts.toolbarFixedTarget === document) { - this.$modal.css({ - width: '96%', - marginTop: (windowHeight/2 - height/2) + 'px' - }); - return; + boxTop = this.$box.offset().top; } - if (height > windowHeight) + if (scrollTop > boxTop) { - this.$modal.css({ - width: this.modal.width + 'px', - marginTop: '20px' - }); + this.toolbar.observeScrollEnable(scrollTop, boxTop); } else { - this.$modal.css({ - width: this.modal.width + 'px', - marginTop: (windowHeight/2 - height/2) + 'px' - }); + this.toolbar.observeScrollDisable(); } }, - showOnMobile: function() + observeScrollEnable: function(scrollTop, boxTop) { - this.$modal.css({ - width: '96%', - marginTop: '2%' + var top = this.opts.toolbarFixedTopOffset + scrollTop - boxTop; + var left = 0; + var end = boxTop + this.$box.height() + 30; + var width = this.$box.innerWidth(); + + this.$toolbar.addClass('toolbar-fixed-box'); + this.$toolbar.css({ + position: 'absolute', + width: width, + top: top + 'px', + left: left }); + this.toolbar.setDropdownsFixed(); + this.$toolbar.css('visibility', (scrollTop < end) ? 'visible' : 'hidden'); }, - resize: function() + observeScrollDisable: function() { - if (this.utils.isMobile()) - { - this.modal.showOnMobile(); - } - else - { - this.modal.showOnDesktop(); - } - }, - setTitle: function(title) - { - this.$modalHeader.html(title); - }, - setContent: function() - { - this.$modalBody.html(this.modal.getTemplate(this.modal.templateName)); - }, - setDraggable: function() - { - if (typeof $.fn.draggable === 'undefined') return; + this.$toolbar.css({ + position: 'relative', + width: 'auto', + top: 0, + left: 0, + visibility: 'visible' + }); - this.$modal.draggable({ handle: this.$modalHeader }); - this.$modalHeader.css('cursor', 'move'); - }, - setEnter: function(e) - { - if (e.which != 13) return; + this.toolbar.unsetDropdownsFixed(); + this.$toolbar.removeClass('toolbar-fixed-box'); - e.preventDefault(); - this.$modal.find('button.redactor-modal-action-btn').click(); }, - createCancelButton: function() + setDropdownsFixed: function() { - var button = $('<button>').addClass('redactor-modal-btn redactor-modal-close-btn').html(this.lang.get('cancel')); - button.on('click', $.proxy(this.modal.close, this)); - - this.$modalFooter.append(button); - }, - createDeleteButton: function(label) - { - return this.modal.createButton(label, 'delete'); - }, - createActionButton: function(label) - { - return this.modal.createButton(label, 'action'); - }, - createButton: function(label, className) - { - var button = $('<button>').addClass('redactor-modal-btn').addClass('redactor-modal-' + className + '-btn').html(label); - this.$modalFooter.append(button); - - return button; - }, - setButtonsWidth: function() - { - var buttons = this.$modalFooter.find('button'); - var buttonsSize = buttons.size(); - if (buttonsSize === 0) return; - - buttons.css('width', (100/buttonsSize) + '%'); - }, - build: function() - { - this.modal.buildOverlay(); - - this.$modalBox = $('<div id="redactor-modal-box" />').hide(); - this.$modal = $('<div id="redactor-modal" />'); - this.$modalHeader = $('<header />'); - this.$modalClose = $('<span id="redactor-modal-close" />').html('&times;'); - this.$modalBody = $('<div id="redactor-modal-body" />'); - this.$modalFooter = $('<footer />'); - - this.$modal.append(this.$modalHeader); - this.$modal.append(this.$modalClose); - this.$modal.append(this.$modalBody); - this.$modal.append(this.$modalFooter); - this.$modalBox.append(this.$modal); - this.$modalBox.appendTo(document.body); - }, - buildOverlay: function() - { - this.$modalOverlay = $('<div id="redactor-modal-overlay">').hide(); - $('body').prepend(this.$modalOverlay); - }, - enableEvents: function() - { - this.$modalClose.on('click.redactor-modal', $.proxy(this.modal.close, this)); - $(document).on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this)); - this.$editor.on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this)); - this.$modalBox.on('click.redactor-modal', $.proxy(this.modal.close, this)); - }, - disableEvents: function() - { - this.$modalClose.off('click.redactor-modal'); - $(document).off('keyup.redactor-modal'); - this.$editor.off('keyup.redactor-modal'); - this.$modalBox.off('click.redactor-modal'); - $(window).off('resize.redactor-modal'); - }, - closeHandler: function(e) - { - if (e.which != this.keyCode.ESC) return; - - this.modal.close(false); - }, - close: function(e) - { - if (e) + var top = this.$toolbar.innerHeight() + this.opts.toolbarFixedTopOffset; + var position = 'fixed'; + if (this.opts.toolbarFixedTarget !== document) { - if (!$(e.target).hasClass('redactor-modal-close-btn') && e.target != this.$modalClose[0] && e.target != this.$modalBox[0]) - { - return; - } - - e.preventDefault(); + top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top) + this.opts.toolbarFixedTopOffset; + position = 'absolute'; } - if (!this.$modalBox) return; - - this.modal.disableEvents(); - - this.$modalOverlay.remove(); - - this.$modalBox.fadeOut('fast', $.proxy(function() + $('.redactor-dropdown').each(function() { - this.$modalBox.remove(); - - setTimeout($.proxy(this.utils.restoreScroll, this), 0); - - if (e !== undefined) this.selection.restore(); - - $(document.body).css('overflow', this.modal.bodyOveflow); - this.core.setCallback('modalClosed', this.modal.templateName); - - }, this)); - - } - }; - }, - progress: function() - { - return { - show: function() - { - $(document.body).append($('<div id="redactor-progress"><span></span></div>')); - $('#redactor-progress').fadeIn(); + $(this).css({ position: position, top: top + 'px' }); + }); }, - hide: function() + unsetDropdownsFixed: function() { - $('#redactor-progress').fadeOut(1500, function() + var top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top); + $('.redactor-dropdown').each(function() { - $(this).remove(); + $(this).css({ position: 'absolute', top: top + 'px' }); }); } - }; }, upload: function() { return { @@ -7501,10 +7835,11 @@ html = html.replace(/[\u200B-\u200D\uFEFF]/g, ''); html = html.replace(/&nbsp;/gi, ''); html = html.replace(/<\/?br\s?\/?>/g, ''); html = html.replace(/\s/g, ''); html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, ''); + html = html.replace(/<iframe(.*?[^>])>$/i, 'iframe'); // remove empty tags if (removeEmptyTags !== false) { html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, ''); @@ -7567,16 +7902,13 @@ }, removeEmpty: function(i, s) { var $s = $(s); - $s.find('.redactor-invisible-space').replaceWith(function() - { - return $(this).contents(); - }); + $s.find('.redactor-invisible-space').removeAttr('style').removeAttr('class'); - if ($s.find('hr, br, img').length !== 0) return; + if ($s.find('hr, br, img, iframe').length !== 0) return; var text = $.trim($s.text()); if (this.utils.isEmpty(text, false)) { $s.remove(); } @@ -7727,10 +8059,14 @@ return false; } return el; }, + isCurrentOrParentHeader: function() + { + return this.utils.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']); + }, isCurrentOrParent: function(tagName) { var parent = this.selection.getParent(); var current = this.selection.getCurrent(); @@ -7752,10 +8088,12 @@ return this.utils.isCurrentOrParentOne(current, parent, tagName); } }, isCurrentOrParentOne: function(current, parent, tagName) { + tagName = tagName.toUpperCase(); + return parent && parent.tagName === tagName ? parent : current && current.tagName === tagName ? current : false; }, // browsers detection @@ -7799,20 +8137,21 @@ Redactor.prototype.init.prototype = Redactor.prototype; // LINKIFY $.Redactor.fn.formatLinkify = function(protocol, convertLinks, convertUrlLinks, convertImageLinks, convertVideoLinks, linkSize) { - var urlCheck = '((?:http[s]?:\\/\\/(?:www\\.)?|www\\.){1}(?:[0-9A-Za-z\\-%_]+\\.)+[a-zA-Z]{2,}(?::[0-9]+)?(?:(?:/[0-9A-Za-z\\-\\.%_]*)+)?(?:\\?(?:[0-9A-Za-z\\-\\.%_]+(?:=[0-9A-Za-z\\-\\.%_\\+]*)?)?(?:&(?:[0-9A-Za-z\\-\\.%_]+(?:=[0-9A-Za-z\\-\\.%_\\+]*)?)?)*)?(?:#[0-9A-Za-z\\-\\.%_\\+=\\?&;]*)?)'; + var urlCheck = '((?:http[s]?:\\/\\/(?:www\\.)?|www\\.){1}(?:[0-9A-Za-z\\-%_]+\\.)+[a-zA-Z]{2,}(?::[0-9]+)?(?:(?:/[0-9A-Za-z\\-#\\.%\+_]*)+)?(?:\\?(?:[0-9A-Za-z\\-\\.%_]+(?:=[0-9A-Za-z\\-\\.%_\\+]*)?)?(?:&(?:[0-9A-Za-z\\-\\.%_]+(?:=[0-9A-Za-z\\-\\.%_\\+]*)?)?)*)?(?:#[0-9A-Za-z\\-\\.%_\\+=\\?&;]*)?)'; var regex = new RegExp(urlCheck, 'gi'); var rProtocol = /(https?|ftp):\/\//i; var urlImage = /(https?:\/\/.*\.(?:png|jpg|jpeg|gif))/gi; var childNodes = (this.$editor ? this.$editor[0] : this).childNodes, i = childNodes.length; while (i--) { var n = childNodes[i]; - if (n.nodeType === 3) + + if (n.nodeType === 3 && n.parentNode !== 'PRE') { var html = n.nodeValue; // youtube & vimeo if (convertVideoLinks && html) @@ -7870,10 +8209,10 @@ } $(n).after(html).remove(); } } - else if (n.nodeType === 1 && !/^(a|button|textarea)$/i.test(n.tagName)) + else if (n.nodeType === 1 && !/^(pre|a|button|textarea)$/i.test(n.tagName)) { $.Redactor.fn.formatLinkify.call(n, protocol, convertLinks, convertUrlLinks, convertImageLinks, convertVideoLinks, linkSize); } } }; \ No newline at end of file