app/assets/javascripts/annex/jquery.redactor.js in annex-cms-0.4.0 vs app/assets/javascripts/annex/jquery.redactor.js in annex-cms-0.5.0

- old
+ new

@@ -1,8 +1,8 @@ /* - Redactor v10.0.6 - Updated: January 7, 2015 + Redactor 10.2.3 + Updated: August 15, 2015 http://imperavi.com/redactor/ Copyright (c) 2009-2015, Imperavi LLC. License: http://imperavi.com/redactor/license/ @@ -10,10 +10,11 @@ Usage: $('#content').redactor(); */ (function($) { + 'use strict'; if (!Function.prototype.bind) { Function.prototype.bind = function(scope) @@ -26,13 +27,10 @@ }; } var uuid = 0; - var reUrlYoutube = /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.\-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig; - var reUrlVimeo = /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/; - // Plugin $.fn.redactor = function(options) { var val = []; var args = Array.prototype.slice.call(arguments, 1); @@ -92,15 +90,15 @@ return new Redactor.prototype.init(el, options); } // Functionality $.Redactor = Redactor; - $.Redactor.VERSION = '10.0.6'; + $.Redactor.VERSION = '10.2.3'; $.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', + 'lang', 'line', 'link', 'linkify', 'list', 'modal', 'observe', 'paragraphize', 'paste', 'placeholder', 'progress', 'selection', 'shortcuts', 'tabifier', 'tidy', 'toolbar', 'upload', 'utils']; $.Redactor.opts = { @@ -133,10 +131,11 @@ autosave: false, // false or url autosaveName: false, autosaveInterval: 60, // seconds autosaveOnChange: false, + autosaveFields: false, linkTooltip: true, linkProtocol: 'http', linkNofollow: false, linkSize: 50, @@ -145,18 +144,18 @@ imageLink: true, imagePosition: true, imageFloatMargin: '10px', imageResizable: true, - imageUpload: false, + imageUpload: null, imageUploadParam: 'file', uploadImageField: false, dragImageUpload: true, - fileUpload: false, + fileUpload: null, fileUploadParam: 'file', dragFileUpload: true, s3: false, @@ -177,11 +176,11 @@ toolbarFixedTarget: document, toolbarFixedTopOffset: 0, // pixels toolbarExternal: false, // ID selector toolbarOverflow: false, - buttonSource: false, + source: true, buttons: ['html', 'formatting', 'bold', 'italic', 'deleted', 'unorderedlist', 'orderedlist', 'outdent', 'indent', 'image', 'file', 'link', 'alignment', 'horizontalrule'], // + 'underline' buttonsHide: [], buttonsHideOnMobile: [], @@ -189,16 +188,21 @@ formatting: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], formattingAdd: false, tabifier: true, - deniedTags: ['html', 'head', 'link', 'body', 'meta', 'script', 'style', 'applet'], + deniedTags: ['script', 'style'], allowedTags: false, // or array + paragraphizeBlocks: ['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'], + removeComments: false, replaceTags: [ - ['strike', 'del'] + ['strike', 'del'], + ['b', 'strong'] ], replaceStyles: [ ['font-weight:\\s?bold', "strong"], ['font-style:\\s?italic', "em"], ['text-decoration:\\s?underline', "u"], @@ -247,12 +251,15 @@ indentValue: 20, verifiedTags: ['a', 'img', 'b', 'strong', 'sub', 'sup', 'i', 'em', 'u', 'small', 'strike', 'del', 'cite', 'ul', 'ol', 'li'], // and for span tag special rule inlineTags: ['strong', 'b', 'u', 'em', 'i', 'code', 'del', 'ins', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small'], alignmentTags: ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DL', 'DT', 'DD', 'DIV', 'TD', 'BLOCKQUOTE', 'OUTPUT', 'FIGCAPTION', 'ADDRESS', 'SECTION', 'HEADER', 'FOOTER', 'ASIDE', 'ARTICLE'], blockLevelElements: ['PRE', 'UL', 'OL', 'LI'], + highContrast: false, + observe: { + dropdowns: [] + }, - // lang langs: { en: { html: 'HTML', video: 'Insert Video', @@ -321,21 +328,34 @@ anchor: 'Anchor', link_new_tab: 'Open link in new tab', underline: 'Underline', alignment: 'Alignment', filename: 'Name (optional)', - edit: 'Edit' + edit: 'Edit', + upload_label: 'Drop file here or ' } - } + }, + + linkify: { + regexps: { + youtube: /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.\-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig, + vimeo: /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/, + image: /((https?|www)[^\s]+\.)(jpe?g|png|gif)(\?[^\s-]+)?/ig, + url: /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/ig, + } + }, + + codemirror: false }; // Functionality Redactor.fn = $.Redactor.prototype = { keyCode: { BACKSPACE: 8, DELETE: 46, + UP: 38, DOWN: 40, ENTER: 13, SPACE: 32, ESC: 27, TAB: 9, @@ -369,10 +389,20 @@ this.reIsBlock = new RegExp('^(' + this.opts.blockLevelElements.join('|' ) + ')$', 'i'); // setup allowed and denied tags this.tidy.setupAllowed(); + // setup denied tags + if (this.opts.deniedTags !== false) + { + var tags = ['html', 'head', 'link', 'body', 'meta', 'applet']; + for (var i = 0; i < tags.length; i++) + { + this.opts.deniedTags.push(tags[i]); + } + } + // load lang this.lang.load(); // extend shortcuts $.extend(this.opts.shortcuts, this.opts.shortcutsAdd); @@ -423,11 +453,10 @@ for (var z = 0; z < len; z++) { this[module][methods[z]] = this[module][methods[z]].bind(this); } }, - alignment: function() { return { left: function() { @@ -445,97 +474,136 @@ { this.alignment.set('justify'); }, set: function(type) { + // focus if (!this.utils.browser('msie')) this.$editor.focus(); this.buffer.set(); this.selection.save(); + // get blocks this.alignment.blocks = this.selection.getBlocks(); - if (this.opts.linebreaks && this.alignment.blocks[0] === false) + this.alignment.type = type; + + // set alignment + if (this.alignment.isLinebreaksOrNoBlocks()) { - this.alignment.setText(type); + this.alignment.setText(); } else { - this.alignment.setBlocks(type); + this.alignment.setBlocks(); } + // sync this.selection.restore(); this.code.sync(); }, - setText: function(type) + setText: function() { var wrapper = this.selection.wrap('div'); - $(wrapper).attr('data-tagblock', 'redactor'); - $(wrapper).css('text-align', type); + $(wrapper).attr('data-tagblock', 'redactor').css('text-align', this.alignment.type); }, - setBlocks: function(type) + setBlocks: function() { $.each(this.alignment.blocks, $.proxy(function(i, el) { var $el = this.utils.getAlignmentElement(el); - if (!$el) return; - if (type === '' && typeof($el.data('tagblock')) !== 'undefined') + if (this.alignment.isNeedReplaceElement($el)) { - $el.replaceWith($el.html()); + this.alignment.replaceElement($el); } else { - $el.css('text-align', type); - this.utils.removeEmptyAttr($el, 'style'); + this.alignment.alignElement($el); } - }, this)); + }, + isLinebreaksOrNoBlocks: function() + { + return (this.opts.linebreaks && this.alignment.blocks[0] === false); + }, + isNeedReplaceElement: function($el) + { + return (this.alignment.type === '' && typeof($el.data('tagblock')) !== 'undefined'); + }, + replaceElement: function($el) + { + $el.replaceWith($el.html()); + }, + alignElement: function($el) + { + $el.css('text-align', this.alignment.type); + this.utils.removeEmptyAttr($el, 'style'); } }; }, autosave: function() { return { + html: false, 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); - } + if (this.opts.autosaveOnChange) return; + this.autosaveInterval = setInterval(this.autosave.load, this.opts.autosaveInterval * 1000); }, onChange: function() { if (!this.opts.autosaveOnChange) return; - this.autosave.load(); }, load: function() { - var html = this.code.get(); - if (this.autosave.html === html) return; - if (this.utils.isEmpty(html)) return; + if (!this.opts.autosave) return; - $.ajax({ + this.autosave.source = this.code.get(); + + if (this.autosave.html === this.autosave.source) return; + + // data + var data = {}; + data['name'] = this.autosave.name; + data[this.autosave.name] = this.autosave.source; + data = this.autosave.getHiddenFields(data); + + // ajax + var jsxhr = $.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) + data: data }); + + jsxhr.done(this.autosave.success); }, - success: function(data, html) + getHiddenFields: function(data) { + if (this.opts.autosaveFields === false || typeof this.opts.autosaveFields !== 'object') + { + return data; + } + + $.each(this.opts.autosaveFields, $.proxy(function(k, v) + { + if (v !== null && v.toString().indexOf('#') === 0) v = $(v).val(); + data[k] = v; + + }, this)); + + return data; + + }, + success: function(data) + { var json; try { json = $.parseJSON(data); } @@ -546,11 +614,11 @@ } var callbackName = (typeof json.error == 'undefined') ? 'autosave' : 'autosaveError'; this.core.setCallback(callbackName, this.autosave.name, json); - this.autosave.html = html; + this.autosave.html = this.autosave.source; }, disable: function() { clearInterval(this.autosaveInterval); } @@ -564,11 +632,11 @@ 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'; + else if (typeof this.formatting[name]['class'] != 'undefined') type = 'class'; if (typeof this.formatting[name].clear != 'undefined') { this.block.clearStyle = true; } @@ -588,10 +656,27 @@ this.block.isRemoveInline = (tag == 'pre' || tag.search(/h[1-6]/i) != -1); // focus if (!this.utils.browser('msie')) this.$editor.focus(); + var html = $.trim(this.$editor.html()); + this.block.isEmpty = this.utils.isEmpty(html); + + // FF focus + if (this.utils.browser('mozilla') && !this.focus.isFocused()) + { + if (this.block.isEmpty) + { + var $first; + if (!this.opts.linebreaks) + { + $first = this.$editor.children().first(); + this.caret.setEnd($first); + } + } + } + this.block.blocks = this.selection.getBlocks(); this.block.blocksSize = this.block.blocks.length; this.block.type = type; this.block.value = value; @@ -601,14 +686,16 @@ this.block.set(tag); this.selection.restore(); this.code.sync(); + this.observe.load(); }, set: function(tag) { + this.selection.get(); this.block.containerTag = this.range.commonAncestorContainer.tagName; if (this.range.collapsed) { @@ -619,10 +706,20 @@ this.block.setMultiple(tag); } }, setCollapsed: function(tag) { + if (this.opts.linebreaks && this.block.isEmpty && tag != 'p') + { + var node = document.createElement(tag); + this.$editor.html(node); + this.caret.setEnd(node); + + return; + } + + var block = this.block.blocks[0]; if (block === false) return; if (block.tagName == 'LI') { @@ -633,22 +730,21 @@ } var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); if (isContainerTable && !this.opts.linebreaks) { - 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>'); + $(block).append('<br>'); this.utils.replaceWithContents(block); } else { var $formatted = this.utils.replaceToTag(block, tag); @@ -665,11 +761,11 @@ else if (tag == 'blockquote' && block.tagName.toLowerCase() == tag) { // blockquote off if (this.opts.linebreaks) { - $(block).prepend('<br>').append('<br>'); + $(block).append('<br>'); this.utils.replaceWithContents(block); } else { var $el = this.utils.replaceToTag(block, 'p'); @@ -679,10 +775,15 @@ else if (block.tagName.toLowerCase() == tag) { this.block.toggle($(block)); } + + if (typeof this.block.type == 'undefined' && typeof this.block.value == 'undefined') + { + $(block).removeAttr('class').removeAttr('style'); + } }, setMultiple: function(tag) { var block = this.block.blocks[0]; var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); @@ -692,11 +793,11 @@ if (block.tagName.toLowerCase() == tag && tag == 'blockquote') { // blockquote off if (this.opts.linebreaks) { - $(block).prepend('<br>').append('<br>'); + $(block).append('<br>'); this.utils.replaceWithContents(block); } else { var $el = this.utils.replaceToTag(block, 'p'); @@ -754,20 +855,26 @@ // only blockquote selected if (count == this.block.blocksSize) { $.each(this.block.blocks, $.proxy(function(i,s) { + var $formatted = false; if (this.opts.linebreaks) { $(s).prepend('<br>').append('<br>'); - this.utils.replaceWithContents(s); + $formatted = this.utils.replaceWithContents(s); } else { - this.utils.replaceToTag(s, 'p'); + $formatted = this.utils.replaceToTag(s, 'p'); } + if ($formatted && typeof this.block.type == 'undefined' && typeof this.block.value == 'undefined') + { + $formatted.removeAttr('class').removeAttr('style'); + } + }, this)); return; } @@ -780,11 +887,11 @@ var classSize = 0; var toggleType = false; if (this.block.type == 'class') { toggleType = 'toggle'; - classSize = $(this.block.blocks).filter('.' + this.block.value).size(); + classSize = $(this.block.blocks).filter('.' + this.block.value).length; if (this.block.blocksSize == classSize) toggleType = 'toggle'; else if (this.block.blocksSize > classSize) toggleType = 'set'; else if (classSize === 0) toggleType = 'set'; @@ -807,11 +914,16 @@ 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(); + if (typeof this.block.type == 'undefined' && typeof this.block.value == 'undefined') + { + $formatted.removeAttr('class').removeAttr('style'); + } + }, this)); } } }, setForce: function($el) @@ -869,11 +981,11 @@ { $el.removeClass(this.block.value); }, formatListToBlockquote: function() { - var block = $(this.block.blocks[0]).closest('ul, ol'); + var block = $(this.block.blocks[0]).closest('ul, ol', this.$editor[0]); $(block).find('ul, ol').contents().unwrap(); $(block).find('li').append($('<br>')).contents().unwrap(); var $el = this.utils.replaceToTag(block, 'blockquote'); @@ -928,15 +1040,10 @@ 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)); @@ -959,17 +1066,28 @@ if (this.opts.linebreaks && tag == 'p') { this.utils.replaceWithContents($formatted); } + if (this.opts.linebreaks) + { + var $next = $formatted.next().next(); + if ($next.size() != 0 && $next[0].tagName === 'BR') + { + $next.remove(); + } + } + + + }, formatTableWrapping: function($formatted) { - if ($formatted.closest('table').size() === 0) return; + if ($formatted.closest('table', this.$editor[0]).length === 0) return; - if ($formatted.closest('tr').size() === 0) $formatted.wrap('<tr>'); - if ($formatted.closest('td').size() === 0 && $formatted.closest('th').size() === 0) + if ($formatted.closest('tr', this.$editor[0]).length === 0) $formatted.wrap('<tr>'); + if ($formatted.closest('td', this.$editor[0]).length === 0 && $formatted.closest('th').length === 0) { $formatted.wrap('<td>'); } }, removeData: function(name, value) @@ -1120,39 +1238,31 @@ build: function() { return { run: function() { - this.build.createContainerBox(); this.build.loadContent(); this.build.loadEditor(); this.build.enableEditor(); this.build.setCodeAndCall(); - }, isTextarea: function() { return (this.$element[0].tagName === 'TEXTAREA'); }, createContainerBox: function() { - this.$box = $('<div class="redactor-box" />'); + this.$box = $('<div class="redactor-box" role="application" />'); }, createTextarea: function() { this.$textarea = $('<textarea />').attr('name', this.build.getTextareaName()); }, getTextareaName: function() { - var name = this.$element.attr('id'); - if (typeof(name) == 'undefined') - { - name = 'content-' + this.uuid; - } - - return name; + return ((typeof(name) == 'undefined')) ? 'content-' + this.uuid : this.$element.attr('id'); }, loadContent: function() { var func = (this.build.isTextarea()) ? 'val' : 'html'; this.content = $.trim(this.$element[func]()); @@ -1191,18 +1301,17 @@ this.build.setOptions(); this.build.callEditor(); // code mode - if (!this.opts.visual) - { - setTimeout($.proxy(this.code.showCode, this), 200); - } + if (this.opts.visual) return; + setTimeout($.proxy(this.code.showCode, this), 200); }, callEditor: function() { this.build.disableMozillaEditing(); + this.build.disableIeLinks(); this.build.setEvents(); this.build.setHelpers(); // load toolbar if (this.opts.toolbar) @@ -1234,64 +1343,67 @@ if (this.opts.minHeight) this.$editor.css('minHeight', this.opts.minHeight); if (this.opts.maxHeight) this.$editor.css('maxHeight', this.opts.maxHeight); }, + setEventDropUpload: function(e) + { + e.preventDefault(); + + if (!this.opts.dragImageUpload || !this.opts.dragFileUpload) return; + + var files = e.dataTransfer.files; + this.upload.directUpload(files[0], e); + }, + setEventDrop: function(e) + { + this.code.sync(); + setTimeout(this.clean.clearUnverified, 1); + this.core.setCallback('drop', e); + }, setEvents: function() { // drop this.$editor.on('drop.redactor', $.proxy(function(e) { e = e.originalEvent || e; if (window.FormData === undefined || !e.dataTransfer) return true; - var length = e.dataTransfer.files.length; - if (length === 0) + if (e.dataTransfer.files.length === 0) { - this.code.sync(); - setTimeout($.proxy(this.clean.clearUnverified, this), 1); - this.core.setCallback('drop', e); - - return true; + return this.build.setEventDrop(e); } else { - e.preventDefault(); - - if (this.opts.dragImageUpload || this.opts.dragFileUpload) - { - var files = e.dataTransfer.files; - this.upload.directUpload(files[0], e); - } + this.build.setEventDropUpload(e); } - setTimeout($.proxy(this.clean.clearUnverified, this), 1); - + setTimeout(this.clean.clearUnverified, 1); this.core.setCallback('drop', e); }, this)); // click this.$editor.on('click.redactor', $.proxy(function(e) { - var type = 'click'; - if ((this.core.getEvent() == 'click' || this.core.getEvent() == 'arrow')) - { - type = false; - } + var event = this.core.getEvent(); + var type = (event == 'click' || event == 'arrow') ? false : 'click'; this.core.addEvent(type); this.utils.disableSelectAll(); this.core.setCallback('click', e); }, this)); // paste this.$editor.on('paste.redactor', $.proxy(this.paste.init, this)); + // cut + this.$editor.on('cut.redactor', $.proxy(this.code.sync, this)); + // keydown this.$editor.on('keydown.redactor', $.proxy(this.keydown.init, this)); // keyup this.$editor.on('keyup.redactor', $.proxy(this.keyup.init, this)); @@ -1307,128 +1419,131 @@ { this.$textarea.on('keyup.redactor-textarea', $.proxy(this.opts.codeKeyupCallback, this)); } // focus - if ($.isFunction(this.opts.focusCallback)) + this.$editor.on('focus.redactor', $.proxy(function(e) { - this.$editor.on('focus.redactor', $.proxy(this.opts.focusCallback, this)); - } + if ($.isFunction(this.opts.focusCallback)) this.core.setCallback('focus', e); - var clickedElement; - $(document).on('mousedown', function(e) { - clickedElement = $(e.target); - }); + if (this.selection.getCurrent() === false) + { + this.selection.get(); + this.range.setStart(this.$editor[0], 0); + this.range.setEnd(this.$editor[0], 0); + this.selection.addRange(); + } + + }, this)); + + // blur - this.$editor.on('blur.redactor', $.proxy(function(e) + $(document).on('mousedown.redactor-blur.' + this.uuid, $.proxy(function(e) { + if (this.start) return; if (this.rtePaste) return; - var $el = $(clickedElement); - if (!$el.hasClass('redactor-toolbar, redactor-dropdown') && !$el.is('#redactor-modal') && $el.parents('.redactor-toolbar, .redactor-dropdown, #redactor-modal').size() === 0) + if ($(e.target).closest('.redactor-editor, .redactor-toolbar, .redactor-dropdown').size() !== 0) { - this.utils.disableSelectAll(); - if ($.isFunction(this.opts.blurCallback)) this.core.setCallback('blur', e); + return; } + + this.utils.disableSelectAll(); + if ($.isFunction(this.opts.blurCallback)) this.core.setCallback('blur', e); + }, this)); + }, setHelpers: function() { - // autosave - this.autosave.enable(); + // linkify + if (this.linkify.isEnabled()) + { + this.linkify.format(); + } // placeholder this.placeholder.enable(); // focus - if (this.opts.focus) setTimeout($.proxy(this.focus.setStart, this), 100); - if (this.opts.focusEnd) setTimeout($.proxy(this.focus.setEnd, this), 100); + if (this.opts.focus) setTimeout(this.focus.setStart, 100); + if (this.opts.focusEnd) setTimeout(this.focus.setEnd, 100); }, plugins: function() { if (!this.opts.plugins) return; - if (!RedactorPlugins) return; $.each(this.opts.plugins, $.proxy(function(i, s) { - if (typeof RedactorPlugins[s] === 'undefined') return; + var func = (typeof RedactorPlugins !== 'undefined' && typeof RedactorPlugins[s] !== 'undefined') ? RedactorPlugins : Redactor.fn; - if ($.inArray(s, $.Redactor.modules) !== -1) + if (!$.isFunction(func[s])) { - $.error('Plugin name "' + s + '" matches the name of the Redactor\'s module.'); return; } - if (!$.isFunction(RedactorPlugins[s])) return; + this[s] = func[s](); - this[s] = RedactorPlugins[s](); - + // get methods var methods = this.getModuleMethods(this[s]); var len = methods.length; // 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(); + if ($.isFunction(this[s].init)) + { + this[s].init(); + } }, this)); - }, disableMozillaEditing: function() { if (!this.utils.browser('mozilla')) return; // FF fix try { document.execCommand('enableObjectResizing', false, false); document.execCommand('enableInlineTableEditing', false, false); } catch (e) {} + }, + disableIeLinks: function() + { + if (!this.utils.browser('msie')) return; + + // IE prevent converting links + document.execCommand("AutoUrlDetect", false, false); } }; }, button: function() { return { build: function(btnName, btnObject) { - var $button = $('<a href="#" class="re-icon re-' + btnName + '" rel="' + btnName + '" />').attr('tabindex', '-1'); + var $button = $('<a href="#" class="re-icon re-' + btnName + '" rel="' + btnName + '" />').attr({'role': 'button', 'aria-label': btnObject.title, 'tabindex': '-1'}); + // click if (btnObject.func || btnObject.command || btnObject.dropdown) { - $button.on('touchstart click', $.proxy(function(e) - { - if ($button.hasClass('redactor-button-disabled')) return false; - - var type = 'func'; - var callback = btnObject.func; - if (btnObject.command) - { - type = 'command'; - callback = btnObject.command; - } - else if (btnObject.dropdown) - { - type = 'dropdown'; - callback = false; - } - - this.button.onClick(e, btnName, type, callback); - - }, this)); + this.button.setEvent($button, btnName, btnObject); } // dropdown if (btnObject.dropdown) { - var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-box-' + btnName + '" style="display: none;">'); + $button.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true); + + var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-box-' + btnName + '" style="display: none;">'); $button.data('dropdown', $dropdown); this.dropdown.build(btnName, $dropdown, btnObject.dropdown); } // tooltip @@ -1437,27 +1552,49 @@ this.button.createTooltip($button, btnName, btnObject.title); } return $button; }, + setEvent: function($button, btnName, btnObject) + { + $button.on('touchstart click', $.proxy(function(e) + { + if ($button.hasClass('redactor-button-disabled')) return false; + + var type = 'func'; + var callback = btnObject.func; + + if (btnObject.command) + { + type = 'command'; + callback = btnObject.command; + } + else if (btnObject.dropdown) + { + type = 'dropdown'; + callback = false; + } + + this.button.onClick(e, btnName, type, callback); + + }, this)); + }, createTooltip: function($button, name, title) { - var $tooltip = $('<span>').addClass('redactor-toolbar-tooltip redactor-toolbar-tooltip-' + name).hide().html(title); + var $tooltip = $('<span>').addClass('redactor-toolbar-tooltip redactor-toolbar-tooltip-' + this.uuid + ' redactor-toolbar-tooltip-' + name).hide().html(title); $tooltip.appendTo('body'); $button.on('mouseover', function() { if ($(this).hasClass('redactor-button-disabled')) return; var pos = $button.offset(); - var height = $button.innerHeight(); - var width = $button.innerWidth(); $tooltip.show(); $tooltip.css({ - top: (pos.top + height) + 'px', - left: (pos.left + width/2 - $tooltip.innerWidth()/2) + 'px' + top: (pos.top + $button.innerHeight()) + 'px', + left: (pos.left + $button.innerWidth()/2 - $tooltip.innerWidth()/2) + 'px' }); }); $button.on('mouseout', function() { @@ -1469,44 +1606,33 @@ { this.button.caretOffset = this.caret.getOffset(); e.preventDefault(); + $(document).find('.redactor-toolbar-tooltip').hide(); + if (this.utils.browser('msie')) e.returnValue = false; - if (type == 'command') + if (type == 'command') this.inline.format(callback); + else if (type == 'dropdown') this.dropdown.show(e, btnName); + else this.button.onClickCallback(e, callback, btnName); + }, + onClickCallback: function(e, callback, btnName) + { + var func; + + if ($.isFunction(callback)) callback.call(this, btnName); + else if (callback.search(/\./) != '-1') { - this.inline.format(callback); - } - else if (type == 'dropdown') - { - this.dropdown.show(e, btnName); - } - else - { - var func; + func = callback.split('.'); + if (typeof this[func[0]] == 'undefined') return; - if ($.isFunction(callback)) - { - callback.call(this, btnName); - this.observe.buttons(e, btnName); - } - else if (callback.search(/\./) != '-1') - { - func = callback.split('.'); - if (typeof this[func[0]] != 'undefined') - { - this[func[0]][func[1]](btnName); - this.observe.buttons(e, btnName); - } - } - else - { - this[callback](btnName); - this.observe.buttons(e, btnName); - } + this[func[0]][func[1]](btnName); } + else this[callback](btnName); + + this.observe.buttons(e, btnName); }, get: function(key) { return this.$toolbar.find('a.re-' + key); }, @@ -1518,26 +1644,26 @@ { this.button.get(key).removeClass('redactor-act'); }, setInactiveAll: function(key) { - if (typeof key == 'undefined') + if (typeof key === 'undefined') { this.$toolbar.find('a.re-icon').removeClass('redactor-act'); } else { this.$toolbar.find('a.re-icon').not('.re-' + key).removeClass('redactor-act'); } }, setActiveInVisual: function() { - this.$toolbar.find('a.re-icon').not('a.re-html').removeClass('redactor-button-disabled'); + this.$toolbar.find('a.re-icon').not('a.re-html, a.re-fullscreen').removeClass('redactor-button-disabled'); }, setInactiveInCode: function() { - this.$toolbar.find('a.re-icon').not('a.re-html').addClass('redactor-button-disabled'); + this.$toolbar.find('a.re-icon').not('a.re-html, a.re-fullscreen').addClass('redactor-button-disabled'); }, changeIcon: function(key, classname) { this.button.get(key).addClass('re-' + classname); }, @@ -1551,10 +1677,12 @@ $button.removeClass('redactor-btn-image').addClass('fa-redactor-btn'); $button.html('<i class="fa ' + name + '"></i>'); }, addCallback: function($btn, callback) { + if ($btn == "buffer") return; + var type = (callback == 'dropdown') ? 'dropdown' : 'func'; var key = $btn.attr('rel'); $btn.on('touchstart click', $.proxy(function(e) { if ($btn.hasClass('redactor-button-disabled')) return false; @@ -1562,27 +1690,29 @@ }, this)); }, addDropdown: function($btn, dropdown) { + $btn.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true); + var key = $btn.attr('rel'); this.button.addCallback($btn, 'dropdown'); - var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-box-' + key + '" style="display: none;">'); + var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-box-' + key + '" style="display: none;">'); $btn.data('dropdown', $dropdown); - if (dropdown) - { - this.dropdown.build(key, $dropdown, dropdown); - } + // build dropdown + if (dropdown) this.dropdown.build(key, $dropdown, dropdown); return $dropdown; }, add: function(key, title) { if (!this.opts.toolbar) return; + if (this.button.isMobileUndoRedo(key)) return "buffer"; + var btn = this.button.build(key, { title: title }); btn.addClass('redactor-btn-image'); this.$toolbar.append($('<li>').append(btn)); @@ -1590,42 +1720,55 @@ }, addFirst: function(key, title) { if (!this.opts.toolbar) return; + if (this.button.isMobileUndoRedo(key)) return "buffer"; + var btn = this.button.build(key, { title: title }); + btn.addClass('redactor-btn-image'); this.$toolbar.prepend($('<li>').append(btn)); return btn; }, addAfter: function(afterkey, key, title) { if (!this.opts.toolbar) return; + if (this.button.isMobileUndoRedo(key)) return "buffer"; + var btn = this.button.build(key, { title: title }); + btn.addClass('redactor-btn-image'); var $btn = this.button.get(afterkey); - if ($btn.size() !== 0) $btn.parent().after($('<li>').append(btn)); + if ($btn.length !== 0) $btn.parent().after($('<li>').append(btn)); else this.$toolbar.append($('<li>').append(btn)); return btn; }, addBefore: function(beforekey, key, title) { if (!this.opts.toolbar) return; + if (this.button.isMobileUndoRedo(key)) return "buffer"; + var btn = this.button.build(key, { title: title }); + btn.addClass('redactor-btn-image'); var $btn = this.button.get(beforekey); - if ($btn.size() !== 0) $btn.parent().before($('<li>').append(btn)); + if ($btn.length !== 0) $btn.parent().before($('<li>').append(btn)); else this.$toolbar.append($('<li>').append(btn)); return btn; }, remove: function(key) { this.button.get(key).remove(); + }, + isMobileUndoRedo: function(key) + { + return (key == "undo" || key == "redo") && !this.utils.isDesktop(); } }; }, caret: function() { @@ -1645,16 +1788,24 @@ this.caret.set(node, 0, node, 0); } }, setEnd: function(node) { + node = node[0] || node; + if (node.lastChild.nodeType == 1) + { + return this.caret.setAfter(node.lastChild); + } + this.caret.set(node, 1, node, 1); + }, set: function(orgn, orgo, focn, foco) { // focus - if (!this.utils.browser('msie')) this.$editor.focus(); + // disabled in 10.0.7 + // if (!this.utils.browser('msie')) this.$editor.focus(); orgn = orgn[0] || orgn; focn = focn[0] || focn; if (this.utils.isBlockTag(orgn.tagName) && orgn.innerHTML === '') @@ -1662,29 +1813,31 @@ orgn.innerHTML = this.opts.invisibleSpace; } if (orgn.tagName == 'BR' && this.opts.linebreaks === false) { - var par = $(this.opts.emptyHtml)[0]; - $(orgn).replaceWith(par); - orgn = par; + var parent = $(this.opts.emptyHtml)[0]; + $(orgn).replaceWith(parent); + orgn = parent; focn = orgn; } this.selection.get(); - try { + try + { this.range.setStart(orgn, orgo); this.range.setEnd(focn, foco); } catch (e) {} this.selection.addRange(); }, setAfter: function(node) { - try { + try + { var tag = $(node)[0].tagName; // inline tag if (tag != 'BR' && !this.utils.isBlock(node)) { @@ -1703,11 +1856,12 @@ { this.caret.setAfterOrBefore(node, 'after'); } } } - catch (e) { + catch (e) + { var space = this.utils.createSpaceElement(); $(node).after(space); this.caret.setEnd(space); } }, @@ -1789,11 +1943,11 @@ var sel = this.selection.get(); var node, offset = 0; var walker = document.createTreeWalker(this.$editor[0], NodeFilter.SHOW_TEXT, null, null); - while (node = walker.nextNode()) + while (node == walker.nextNode()) { offset += node.nodeValue.length; if (offset > start) { this.range.setStart(node, node.nodeValue.length + start - offset); @@ -1808,10 +1962,11 @@ } this.range.collapse(false); this.selection.addRange(); }, + // deprecated setToPoint: function(start, end) { this.caret.setOffset(start, end); }, getCoords: function() @@ -1830,15 +1985,15 @@ // 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, '\''); - if (this.opts.replaceDivs) html = this.clean.replaceDivs(html); + // replace special characters in links + html = html.replace(/<a href="(.*?[^>]?)®(.*?[^>]?)">/gi, '<a href="$1&reg$2">'); + + if (this.opts.replaceDivs && !this.opts.linebreaks) html = this.clean.replaceDivs(html); if (this.opts.linebreaks) html = this.clean.replaceParagraphsToBr(html); // save form tag html = this.clean.saveFormTags(html); @@ -1855,14 +2010,15 @@ return $span.append($el.contents()); }); html = $div.html(); } + $div.remove(); // remove font tag - html = html.replace(/<font(.*?[^<])>/gi, ''); + html = html.replace(/<font(.*?)>/gi, ''); html = html.replace(/<\/font>/gi, ''); // tidy html html = this.tidy.load(html); @@ -1873,16 +2029,18 @@ html = this.clean.setVerified(html); // convert inline tags html = this.clean.convertInline(html); + html = html.replace(/&amp;/g, '&'); + return html; }, onSync: function(html) { // remove spaces - html = html.replace(/[\u200B-\u200D\uFEFF]/g, ''); + html = html.replace(/\u200B/g, ''); html = html.replace(/&#x200b;/gi, ''); if (this.opts.cleanSpaces) { html = html.replace(/&nbsp;/gi, ' '); @@ -1910,30 +2068,54 @@ $.each(chars, function(i,s) { html = html.replace(new RegExp(i, 'g'), s); }); - // remove br in the of li + // remove last br in FF + if (this.utils.browser('mozilla')) + { + html = html.replace(/<br\s?\/?>$/gi, ''); + } + + // remove br in|of li tags html = html.replace(new RegExp('<br\\s?/?></li>', 'gi'), '</li>'); html = html.replace(new RegExp('</li><br\\s?/?>', 'gi'), '</li>'); + + // remove empty attributes + html = html.replace(/<(.*?)rel="\s*?"(.*?[^>]?)>/gi, '<$1$2">'); + html = html.replace(/<(.*?)style="\s*?"(.*?[^>]?)>/gi, '<$1$2">'); + html = html.replace(/="">/gi, '>'); + html = html.replace(/""">/gi, '">'); + html = html.replace(/"">/gi, '">'); + // remove verified - 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(.*?[^>])\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(/<div(.*?)data-tagblock="redactor"(.*?[^>])>/gi, '<div$1$2>'); + html = html.replace(/<(.*?) data-verified="redactor"(.*?[^>])>/gi, '<$1$2>'); + + var $div = $("<div/>").html($.parseHTML(html, document, true)); + $div.find("span").removeAttr("rel"); + + $div.find('pre .redactor-invisible-space').each(function() + { + $(this).contents().unwrap(); + }); + + html = $div.html(); + + // remove rel attribute from img + html = html.replace(/<img(.*?[^>])rel="(.*?[^>])"(.*?[^>])>/gi, '<img$1$3>'); + html = html.replace(/<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, ''); html = html.replace(/<\/font>/gi, ''); // tidy html html = this.tidy.load(html); @@ -1943,29 +2125,25 @@ html = html.replace(/<a(.*?)rel="nofollow"(.*?[^>])>/gi, '<a$1$2>'); html = html.replace(/<a(.*?[^>])>/gi, '<a$1 rel="nofollow">'); } // reconvert inline - html = html.replace(/<(.*?) data-redactor-tag="(.*?)"(.*?[^>])>/gi, '<$1$3>'); - html = html.replace(/<(.*?) data-redactor-class="(.*?)"(.*?[^>])>/gi, '<$1$3>'); - html = html.replace(/<(.*?) data-redactor-style="(.*?)"(.*?[^>])>/gi, '<$1$3>'); + html = html.replace(/\sdata-redactor-(tag|class|style)="(.*?[^>])"/gi, ''); html = html.replace(new RegExp('<(.*?) data-verified="redactor"(.*?[^>])>', 'gi'), '<$1$2>'); html = html.replace(new RegExp('<(.*?) data-verified="redactor">', 'gi'), '<$1>'); + html = html.replace(/&amp;/g, '&'); + return html; }, onPaste: function(html, setMode) { html = $.trim(html); - html = html.replace(/\$/g, '&#36;'); - html = html.replace(/”/g, '"'); - html = html.replace(/“/g, '"'); - html = html.replace(/‘/g, '\''); - html = html.replace(/’/g, '\''); // convert dirty spaces + html = html.replace(/<span class="s[0-9]">/gi, '<span>'); html = html.replace(/<span class="Apple-converted-space">&nbsp;<\/span>/gi, ' '); html = html.replace(/<span class="Apple-tab-span"[^>]*>\t<\/span>/gi, '\t'); html = html.replace(/<span[^>]*>(\s|&nbsp;)<\/span>/gi, ' '); if (this.opts.pastePlainText) @@ -1980,10 +2158,15 @@ return this.clean.getPlainText(html, false); } if (this.utils.isCurrentOrParent('PRE')) { + html = html.replace(/”/g, '"'); + html = html.replace(/“/g, '"'); + html = html.replace(/‘/g, '\''); + html = html.replace(/’/g, '\''); + return this.clean.getPreCode(html); } if (this.utils.isCurrentOrParent(['BLOCKQUOTE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'])) { @@ -2045,10 +2228,11 @@ html = this.clean.removeDirtyStyles(html); html = this.clean.onPasteRemoveSpans(html); html = this.clean.onPasteRemoveEmpty(html); + html = this.clean.convertInline(html); return html; }, onPasteWord: function(html) @@ -2057,24 +2241,112 @@ html = html.replace(/<!--[\s\S]*?-->/gi, ''); // style html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ''); - if (/(class=\"?Mso|style=\"[^\"]*\bmso\-|w:WordDocument)/.test(html)) + // op + html = html.replace(/<o\:p[^>]*>[\s\S]*?<\/o\:p>/gi, ''); + + if (html.match(/class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i)) { + // comments + html = html.replace(/<!--[\s\S]+?-->/gi, ''); + + // scripts + html = html.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, ''); + + // Convert <s> into <strike> + html = html.replace(/<(\/?)s>/gi, "<$1strike>"); + + // Replace nbsp entites to char since it's easier to handle + html = html.replace(/ /gi, ' '); + + // Convert <span style="mso-spacerun:yes">___</span> to string of alternating + // breaking/non-breaking spaces of same length + html = html.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, function(str, spaces) { + return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''; + }); + html = this.clean.onPasteIeFixLinks(html); // shapes html = html.replace(/<img(.*?)v:shapes=(.*?)>/gi, ''); html = html.replace(/src="file\:\/\/(.*?)"/, 'src=""'); - // 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>'); + // lists + var $div = $("<div/>").html(html); + + var lastList = false; + var lastLevel = 1; + var listsIds = []; + + $div.find("p[style]").each(function() + { + var matches = $(this).attr('style').match(/mso\-list\:l([0-9]+)\slevel([0-9]+)/); + + if (matches) + { + var currentList = parseInt(matches[1]); + var currentLevel = parseInt(matches[2]); + var listType = $(this).html().match(/^[\w]+\./) ? "ol" : "ul"; + + var $li = $("<li/>").html($(this).html()); + + $li.html($li.html().replace(/^([\w\.]+)</, '<')); + $li.find("span:first").remove(); + + if (currentLevel == 1 && $.inArray(currentList, listsIds) == -1) + { + var $list = $("<" + listType + "/>").attr({"data-level": currentLevel, + "data-list": currentList}) + .html($li); + + $(this).replaceWith($list); + + lastList = currentList; + listsIds.push(currentList); + } + else + { + if (currentLevel > lastLevel) + { + var $prevList = $div.find('[data-level="' + lastLevel + '"][data-list="' + lastList + '"]'); + + var $lastList = $prevList; + + for(var i = lastLevel; i < currentLevel; i++) + { + $list = $("<" + listType + "/>"); + + $list.appendTo($lastList.find("li").last()); + + $lastList = $list; + } + + $lastList.attr({"data-level": currentLevel, + "data-list": currentList}) + .html($li); + + } + else + { + var $prevList = $div.find('[data-level="' + currentLevel + '"][data-list="' + currentList + '"]').last(); + + $prevList.append($li); + } + + lastLevel = currentLevel; + lastList = currentList; + + $(this).remove(); + } + } + }); + + $div.find('[data-level][data-list]').removeAttr('data-level data-list'); + html = $div.html(); + // remove ms word's bullet html = html.replace(/·/g, ''); html = html.replace(/<p class="Mso(.*?)"/gi, '<p'); // classes @@ -2089,16 +2361,10 @@ // 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, ' '); - } - return html; }, onPasteExtra: function(html) { // remove google docs markers @@ -2189,22 +2455,20 @@ tags = ['ul', 'ol', 'li', 'span', 'a', 'small', 'em', 'strong', 'code', 'kbd', 'mark', 'cite', 'var', 'samp', 'dfn', 'sup', 'sub', 'b', 'i', 'u', 'del', 'br', 'iframe', 'video', 'audio', 'embed', 'param', 'object', 'img']; } var options = { - deniedTags: false, - allowedTags: tags, + deniedTags: (this.opts.deniedTags) ? this.opts.deniedTags : false, + allowedTags: (this.opts.allowedTags) ? this.opts.allowedTags : tags, removeComments: true, removePhp: true, - removeAttr: false, - allowedAttr: attrAllowed, + removeAttr: (this.opts.removeAttr) ? this.opts.removeAttr : false, + allowedAttr: (this.opts.allowedAttr) ? this.opts.allowedAttr : attrAllowed, removeEmpty: tagsEmpty }; - return this.tidy.load(html, options); - }, onPasteRemoveEmpty: function(html) { html = html.replace(/<(p|h[1-6])>(|\s|\n|\t|<br\s?\/?>)<\/(p|h[1-6])>/gi, ''); @@ -2244,12 +2508,12 @@ var matchContainers = html.match(/<\/(p|div)>/gi); if (!matchBlocks && (matchContainers === null || (matchContainers && matchContainers.length <= 1))) { var matchBR = html.match(/<br\s?\/?>/gi); - var matchIMG = html.match(/<img(.*?[^>])>/gi); - if (!matchBR && !matchIMG) + //var matchIMG = html.match(/<img(.*?[^>])>/gi); + if (!matchBR) { this.clean.singleLine = true; html = html.replace(/<\/?(p|div)(.*?)>/gi, ''); } } @@ -2266,37 +2530,73 @@ return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; }); }, savePreCode: function(html) { - var pre = html.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/gi); + html = this.clean.savePreFormatting(html); + html = this.clean.saveCodeFormatting(html); + + html = this.clean.restoreSelectionMarker(html); + + return html; + }, + savePreFormatting: function(html) + { + var pre = html.match(/<pre(.*?)>([\w\W]*?)<\/pre>/gi); + if (pre !== null) { $.each(pre, $.proxy(function(i,s) { - var arr = s.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/i); + var arr = s.match(/<pre(.*?)>([\w\W]*?)<\/pre>/i); - arr[3] = arr[3].replace(/<br\s?\/?>/g, '\n'); - arr[3] = arr[3].replace(/&nbsp;/g, ' '); + arr[2] = arr[2].replace(/<br\s?\/?>/g, '\n'); + arr[2] = arr[2].replace(/&nbsp;/g, ' '); if (this.opts.preSpaces) { - arr[3] = arr[3].replace(/\t/g, Array(this.opts.preSpaces + 1).join(' ')); + arr[2] = arr[2].replace(/\t/g, Array(this.opts.preSpaces + 1).join(' ')); } - arr[3] = this.clean.encodeEntities(arr[3]); + arr[2] = this.clean.encodeEntities(arr[2]); // $ fix - arr[3] = arr[3].replace(/\$/g, '&#36;'); + arr[2] = arr[2].replace(/\$/g, '&#36;'); - html = html.replace(s, '<' + arr[1] + arr[2] + '>' + arr[3] + '</' + arr[1] + '>'); + html = html.replace(s, '<pre' + arr[1] + '>' + arr[2] + '</pre>'); }, this)); } return html; }, + saveCodeFormatting: function(html) + { + var code = html.match(/<code(.*?)>([\w\W]*?)<\/code>/gi); + + if (code !== null) + { + $.each(code, $.proxy(function(i,s) + { + var arr = s.match(/<code(.*?)>([\w\W]*?)<\/code>/i); + + arr[2] = arr[2].replace(/&nbsp;/g, ' '); + arr[2] = this.clean.encodeEntities(arr[2]); + arr[2] = arr[2].replace(/\$/g, '&#36;'); + + html = html.replace(s, '<code' + arr[1] + '>' + arr[2] + '</code>'); + }, this)); + } + + return html; + }, + restoreSelectionMarker: function(html) + { + html = html.replace(/&lt;span id=&quot;selection-marker-([0-9])&quot; class=&quot;redactor-selection-marker&quot; data-verified=&quot;redactor&quot;&gt;​&lt;\/span&gt;/g, '<span id="selection-marker-$1" class="redactor-selection-marker" data-verified="redactor">​</span>'); + + return html; + }, getTextFromHtml: function(html) { html = html.replace(/<br\s?\/?>|<\/H[1-6]>|<\/p>|<\/div>|<\/li>|<\/td>/gi, '\n'); var tmp = document.createElement('div'); @@ -2308,11 +2608,11 @@ getPlainText: function(html, paragraphize) { html = this.clean.getTextFromHtml(html); html = html.replace(/\n/g, '<br />'); - if (this.opts.paragraphize && typeof paragraphize == 'undefined') + if (this.opts.paragraphize && typeof paragraphize == 'undefined' && !this.utils.browser('mozilla')) { html = this.paragraphize.load(html); } return html; @@ -2397,26 +2697,33 @@ var $s = $(s); $s.attr('style', $s.attr('rel')); }); }, + cleanEmptyParagraph: function() + { + + }, setVerified: function(html) { if (this.utils.browser('msie')) return html; html = html.replace(new RegExp('<img(.*?[^>])>', 'gi'), '<img$1 data-verified="redactor">'); - html = html.replace(new RegExp('<span(.*?)>', 'gi'), '<span$1 data-verified="redactor">'); + html = html.replace(new RegExp('<span(.*?[^>])>', 'gi'), '<span$1 data-verified="redactor">'); var matches = html.match(new RegExp('<(span|img)(.*?)style="(.*?)"(.*?[^>])>', 'gi')); + if (matches) { var len = matches.length; for (var i = 0; i < len; i++) { try { + var newTag = matches[i].replace(/style="(.*?)"/i, 'style="$1" rel="$1"'); - html = html.replace(new RegExp(matches[i], 'gi'), newTag); + html = html.replace(matches[i], newTag); + } catch (e) {} } } @@ -2467,11 +2774,11 @@ html = html.replace(/\n\s*\n/g, "\n"); html = html.replace(/^[\s\n]*/g, ' '); html = html.replace(/[\s\n]*$/g, ' '); html = html.replace( />\s{2,}</g, '> <'); // between inline tags can be only one space html = html.replace(/\n\n/g, "\n"); - html = html.replace(/[\u200B-\u200D\uFEFF]/g, ''); + html = html.replace(/\u200B/g, ''); return html; }, replaceDivs: function(html) { @@ -2483,10 +2790,13 @@ else { html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '<p$1>$2</p>'); } + html = html.replace(/<div(.*?[^>])>/gi, ''); + html = html.replace(/<\/div>/gi, ''); + return html; }, replaceDivsToBr: function(html) { html = html.replace(/<div\s(.*?)>/gi, '<p>'); @@ -2522,21 +2832,32 @@ html = $.trim(html.toString()); // clean html = this.clean.onSet(html); + + if (this.utils.browser('msie')) + { + html = html.replace(/<span(.*?)id="selection-marker-(1|2)"(.*?)><\/span>/gi, ''); + } + this.$editor.html(html); this.code.sync(); + if (html !== '') this.placeholder.remove(); + setTimeout($.proxy(this.buffer.add, this), 15); if (this.start === false) this.observe.load(); }, get: function() { var code = this.$textarea.val(); + if (this.opts.replaceDivs) code = this.clean.replaceDivs(code); + if (this.opts.linebreaks) code = this.clean.replaceParagraphsToBr(code); + // indent code code = this.tabifier.get(code); return code; }, @@ -2575,12 +2896,26 @@ this.core.setCallback('change', html); } this.start = false; - // autosave on change + if (this.autosave.html == false) + { + this.autosave.html = this.code.get(); + } + + if (this.opts.codemirror) + { + this.$textarea.next('.CodeMirror').each(function(i, el) + { + el.CodeMirror.setValue(html); + }); + } + + //autosave this.autosave.onChange(); + this.autosave.enable(); }, toggle: function() { if (this.opts.visual) { @@ -2591,68 +2926,177 @@ this.code.showVisual(); } }, showCode: function() { + this.selection.save(); + this.code.offset = this.caret.getOffset(); var scroll = $(window).scrollTop(); - var height = this.$editor.innerHeight(); + var width = this.$editor.innerWidth(), + height = this.$editor.innerHeight(); this.$editor.hide(); var html = this.$textarea.val(); + this.modified = this.clean.removeSpaces(html); // indent code html = this.tabifier.get(html); - this.$textarea.val(html).height(height).show().focus(); - this.$textarea.on('keydown.redactor-textarea-indenting', this.code.textareaIndenting); + // caret position sync + var start = 0, end = 0; + var $editorDiv = $("<div/>").append($.parseHTML(this.clean.onSync(this.$editor.html()), document, true)); + var $selectionMarkers = $editorDiv.find("span.redactor-selection-marker"); - $(window).scrollTop(scroll); + if ($selectionMarkers.length > 0) + { + var editorHtml = this.tabifier.get($editorDiv.html()).replace(/&amp;/g, '&'); - if (this.$textarea[0].setSelectionRange) + if ($selectionMarkers.length == 1) + { + start = this.utils.strpos(editorHtml, $editorDiv.find("#selection-marker-1").prop("outerHTML")); + end = start; + } + else if ($selectionMarkers.length == 2) + { + start = this.utils.strpos(editorHtml, $editorDiv.find("#selection-marker-1").prop("outerHTML")); + end = this.utils.strpos(editorHtml, $editorDiv.find("#selection-marker-2").prop("outerHTML")) - $editorDiv.find("#selection-marker-1").prop("outerHTML").toString().length; + } + } + + this.selection.removeMarkers(); + this.$textarea.val(html); + + if (this.opts.codemirror) { - this.$textarea[0].setSelectionRange(0, 0); + this.$textarea.next('.CodeMirror').each(function(i, el) + { + $(el).show(); + el.CodeMirror.setValue(html); + el.CodeMirror.setSize('100%', height); + el.CodeMirror.refresh(); + + if (start == end) + { + el.CodeMirror.setCursor(el.CodeMirror.posFromIndex(start).line, el.CodeMirror.posFromIndex(end).ch); + } + else + { + el.CodeMirror.setSelection({line: el.CodeMirror.posFromIndex(start).line, + ch: el.CodeMirror.posFromIndex(start).ch}, + {line: el.CodeMirror.posFromIndex(end).line, + ch: el.CodeMirror.posFromIndex(end).ch}); + } + + el.CodeMirror.focus(); + }); } + else + { + this.$textarea.height(height).show().focus(); + this.$textarea.on('keydown.redactor-textarea-indenting', this.code.textareaIndenting); - this.$textarea[0].scrollTop = 0; + $(window).scrollTop(scroll); + if (this.$textarea[0].setSelectionRange) + { + this.$textarea[0].setSelectionRange(start, end); + } + + this.$textarea[0].scrollTop = 0; + } + this.opts.visual = false; this.button.setInactiveInCode(); this.button.setActive('html'); this.core.setCallback('source', html); }, showVisual: function() { + var html; + if (this.opts.visual) return; - var html = this.$textarea.hide().val(); + var start = 0, end = 0; + if (this.opts.codemirror) + { + var selection; + + this.$textarea.next('.CodeMirror').each(function(i, el) + { + selection = el.CodeMirror.listSelections(); + + start = el.CodeMirror.indexFromPos(selection[0].anchor); + end = el.CodeMirror.indexFromPos(selection[0].head); + + html = el.CodeMirror.getValue(); + }); + } + else + { + start = this.$textarea.get(0).selectionStart; + end = this.$textarea.get(0).selectionEnd; + + html = this.$textarea.hide().val(); + } + + // if selection starts from end + if (start > end && end > 0) + { + var tempStart = end; + var tempEnd = start; + + start = tempStart; + end = tempEnd; + } + + start = this.code.enlargeOffset(html, start); + end = this.code.enlargeOffset(html, end); + + html = html.substr(0, start) + this.selection.getMarkerAsHtml(1) + html.substr(start); + + if (end > start) + { + var markerLength = this.selection.getMarkerAsHtml(1).toString().length; + + html = html.substr(0, end + markerLength) + this.selection.getMarkerAsHtml(2) + html.substr(end + markerLength); + } + + + if (this.modified !== this.clean.removeSpaces(html)) { this.code.set(html); + } + if (this.opts.codemirror) + { + this.$textarea.next('.CodeMirror').hide(); + } + this.$editor.show(); if (!this.utils.isEmpty(html)) { this.placeholder.remove(); } - this.caret.setOffset(this.code.offset); + this.selection.restore(); this.$textarea.off('keydown.redactor-textarea-indenting'); this.button.setActiveInVisual(); this.button.setInactive('html'); - this.observe.load(); this.opts.visual = true; + this.core.setCallback('visual', html); }, textareaIndenting: function(e) { if (e.keyCode !== 9) return true; @@ -2660,10 +3104,39 @@ 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; + }, + enlargeOffset: function(html, offset) + { + var htmlLength = html.length; + var c = 0; + + if (html[offset] == '>') + { + c++; + } + else + { + for(var i = offset; i <= htmlLength; i++) + { + c++; + + if (html[i] == '>') + { + break; + } + else if (html[i] == '<' || i == htmlLength) + { + c = 0; + break; + } + } + } + + return offset + c; } }; }, core: function() { @@ -2700,11 +3173,35 @@ { return this.core.event; }, setCallback: function(type, e, data) { - var callback = this.opts[type + 'Callback']; + var eventName = type + 'Callback'; + var eventNamespace = 'redactor'; + var callback = this.opts[eventName]; + + if (this.$textarea) + { + var returnValue = false; + var events = $._data(this.$textarea[0], 'events'); + + if (typeof events != 'undefined' && typeof events[eventName] != 'undefined') + { + $.each(events[eventName], $.proxy(function(key, value) + { + if (value['namespace'] == eventNamespace) + { + var data = (typeof data == 'undefined') ? [e] : [e, data]; + + returnValue = (typeof data == 'undefined') ? value.handler.call(this, e) : value.handler.call(this, e, data); + } + }, this)); + } + + if (returnValue) return returnValue; + } + if ($.isFunction(callback)) { return (typeof data == 'undefined') ? callback.call(this, e) : callback.call(this, e, data); } else @@ -2712,22 +3209,46 @@ return (typeof data == 'undefined') ? e : data; } }, destroy: function() { + this.opts.destroyed = true; + this.core.setCallback('destroy'); // off events and remove data this.$element.off('.redactor').removeData('redactor'); this.$editor.off('.redactor'); + $(document).off('mousedown.redactor-blur.' + this.uuid); + $(document).off('mousedown.redactor.' + this.uuid); + $(document).off('click.redactor-image-delete.' + this.uuid); + $(document).off('click.redactor-image-resize-hide.' + this.uuid); + $(document).off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid); + $("body").off('scroll.redactor.' + this.uuid); + $(this.opts.toolbarFixedTarget).off('scroll.redactor.' + this.uuid); + // common this.$editor.removeClass('redactor-editor redactor-linebreaks redactor-placeholder'); this.$editor.removeAttr('contenteditable'); var html = this.code.get(); + if (this.opts.toolbar) + { + // dropdowns off + this.$toolbar.find('a').each(function() + { + var $el = $(this); + if ($el.data('dropdown')) + { + $el.data('dropdown').remove(); + $el.data('dropdown', {}); + } + }); + } + if (this.build.isTextarea()) { this.$box.after(this.$element); this.$box.remove(); this.$element.val(html).show(); @@ -2745,15 +3266,14 @@ // modal if (this.$modalBox) this.$modalBox.remove(); if (this.$modalOverlay) this.$modalOverlay.remove(); // buttons tooltip - $('.redactor-toolbar-tooltip').remove(); + $('.redactor-toolbar-tooltip-' + this.uuid).remove(); // autosave clearInterval(this.autosaveInterval); - } }; }, dropdown: function() { @@ -2762,25 +3282,35 @@ { if (name == 'formatting' && this.opts.formattingAdd) { $.each(this.opts.formattingAdd, $.proxy(function(i,s) { - var name = s.tag; - if (typeof s.class != 'undefined') + var name = s.tag, + func; + + if (typeof s['class'] != 'undefined') { - name = name + '-' + s.class; + name = name + '-' + s['class']; } s.type = (this.utils.isBlockTag(s.tag)) ? 'block' : 'inline'; - var func = (s.type == 'inline') ? 'inline.formatting' : 'block.formatting'; + if (typeof s.func !== "undefined") + { + func = s.func; + } + else + { + 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, + 'class': s['class'], attr: s.attr, data: s.data, clear: s.clear }; @@ -2788,20 +3318,21 @@ func: func, title: s.title }; }, this)); - } $.each(dropdownObject, $.proxy(function(btnName, btnObject) { - var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '">' + btnObject.title + '</a>'); + var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '" role="button">' + btnObject.title + '</a>'); if (name == 'formatting') $item.addClass('redactor-formatting-' + btnName); $item.on('click', $.proxy(function(e) { + e.preventDefault(); + var type = 'func'; var callback = btnObject.func; if (btnObject.command) { type = 'command'; @@ -2811,14 +3342,19 @@ { type = 'dropdown'; callback = btnObject.dropdown; } + if ($(e.target).hasClass('redactor-dropdown-link-inactive')) return; + this.button.onClick(e, btnName, type, callback); + this.dropdown.hideAll(); }, this)); + this.observe.addDropdown($item, btnName, btnObject); + $dropdown.append($item); }, this)); }, show: function(e, key) @@ -2832,23 +3368,24 @@ var $button = this.button.get(key); // 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); - // ios keyboard hide - if (this.utils.isMobile() && !this.utils.browser('msie')) + if (this.opts.highContrast) { - document.activeElement.blur(); + $dropdown.addClass("redactor-dropdown-contrast"); } if ($button.hasClass('dropact')) { this.dropdown.hideAll(); } else { this.dropdown.hideAll(); + this.observe.dropdowns(); + this.core.setCallback('dropdownShow', { dropdown: $dropdown, key: key, button: $button }); this.button.setActive(key); $button.addClass('dropact'); @@ -2857,11 +3394,11 @@ // fix right placement var dropdownWidth = $dropdown.width(); if ((keyPosition.left + dropdownWidth) > $(document).width()) { - keyPosition.left -= dropdownWidth; + keyPosition.left = Math.max(0, keyPosition.left - dropdownWidth); } var left = keyPosition.left + 'px'; if (this.$toolbar.hasClass('toolbar-fixed-box')) { @@ -2880,51 +3417,60 @@ 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 }); + + this.$dropdown = $dropdown; } - $(document).one('click', $.proxy(this.dropdown.hide, this)); - this.$editor.one('click', $.proxy(this.dropdown.hide, this)); + $(document).one('click.redactor-dropdown', $.proxy(this.dropdown.hide, this)); + this.$editor.one('click.redactor-dropdown', $.proxy(this.dropdown.hide, this)); + $(document).one('keyup.redactor-dropdown', $.proxy(this.dropdown.closeHandler, this)); + // disable scroll whan dropdown scroll - var $body = $(document.body); - var width = $body.width(); + $dropdown.on('mouseover.redactor-dropdown', $.proxy(this.utils.disableBodyScroll, this)).on('mouseout.redactor-dropdown', $.proxy(this.utils.enableBodyScroll, this)); - $dropdown.on('mouseover', function() { - $body.addClass('body-redactor-hidden'); - $body.css('margin-right', ($body.width() - width) + 'px'); + e.stopPropagation(); + }, + closeHandler: function(e) + { + if (e.which != this.keyCode.ESC) return; - }); + this.dropdown.hideAll(); + this.$editor.focus(); + }, + hideAll: function() + { + this.$toolbar.find('a.dropact').removeClass('redactor-act').removeClass('dropact'); - $dropdown.on('mouseout', function() { + this.utils.enableBodyScroll(); - $body.removeClass('body-redactor-hidden').css('margin-right', 0); + $('.redactor-dropdown-' + this.uuid).hide(); + $('.redactor-dropdown-link-selected').removeClass('redactor-dropdown-link-selected'); - }); + if (this.$dropdown) + { + this.$dropdown.off('.redactor-dropdown'); + this.core.setCallback('dropdownHide', this.$dropdown); - e.stopPropagation(); + this.$dropdown = false; + } }, - hideAll: function() - { - this.$toolbar.find('a.dropact').removeClass('redactor-act').removeClass('dropact'); - - $(document.body).removeClass('body-redactor-hidden').css('margin-right', 0); - $('.redactor-dropdown').hide(); - this.core.setCallback('dropdownHide'); - }, hide: function (e) { var $dropdown = $(e.target); - if (!$dropdown.hasClass('dropact')) + + if (!$dropdown.hasClass('dropact') && !$dropdown.hasClass('redactor-dropdown-link-inactive')) { $dropdown.removeClass('dropact'); + $dropdown.off('mouseover mouseout'); + this.dropdown.hideAll(); } } }; }, @@ -2986,11 +3532,11 @@ this.insert.htmlWithoutClean(link); if (typeof json == 'string') return; var linkmarker = $(this.$editor.find('a#filelink-marker')); - if (linkmarker.size() !== 0) + if (linkmarker.length !== 0) { linkmarker.removeAttr('id').removeAttr('style'); } else linkmarker = false; @@ -3006,20 +3552,19 @@ { this.$editor.focus(); var first = this.$editor.children().first(); - if (first.size() === 0) return; + if (first.length === 0) return; if (first[0].length === 0 || first[0].tagName == 'BR' || first[0].nodeType == 3) { return; } if (first[0].tagName == 'UL' || first[0].tagName == 'OL') { - first = first.find('li').first(); - var child = first.children().first(); + var child = first.find('li').first(); if (!this.utils.isBlock(child) && child.text() === '') { // empty inline tag in li this.caret.setStart(child); return; @@ -3039,28 +3584,31 @@ // if node is tag this.caret.setStart(first); }, setEnd: function() { - if (this.utils.browser('mozilla') || this.utils.browser('msie')) + var last = this.$editor.children().last(); + this.$editor.focus(); + + if (last.size() === 0) return; + if (this.utils.isEmpty(this.$editor.html())) { - var last = this.$editor.children().last(); - this.caret.setEnd(last); + + this.selection.get(); + this.range.collapse(true); + this.range.setStartAfter(last[0]); + this.range.setEnd(last[0], 0); + this.selection.addRange(); } else { this.selection.get(); + this.range.selectNodeContents(last[0]); + this.range.collapse(false); + this.selection.addRange(); - try { - this.range.selectNodeContents(this.$editor[0]); - this.range.collapse(false); - - this.selection.addRange(); - } - catch (e) {} } - }, isFocused: function() { var focusNode = document.getSelection().focusNode; if (focusNode === null) return false; @@ -3084,11 +3632,11 @@ this.modal.show(); }, showEdit: function($image) { - var $link = $image.closest('a'); + var $link = $image.closest('a', this.$editor[0]); this.modal.load('imageEdit', this.lang.get('edit'), 705); this.modal.createCancelButton(); this.image.buttonDelete = this.modal.createDeleteButton(this.lang.get('_delete')); @@ -3104,20 +3652,22 @@ { this.image.update($image); }, this)); + // hide link's tooltip + $('.redactor-link-tooltip').remove(); $('#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) + if ($link.length !== 0) { $redactorImageLink.val($link.attr('href')); if ($link.attr('target') == '_blank') $('#redactor-image-link-blank').prop('checked', true); } } @@ -3128,10 +3678,11 @@ var floatValue = ($image.css('display') == 'block' && $image.css('float') == 'none') ? 'center' : $image.css('float'); $('#redactor-image-align').val(floatValue); } this.modal.show(); + $('#redactor-image-title').focus(); }, setFloating: function($image) { var floating = $('#redactor-image-align').val(); @@ -3162,23 +3713,35 @@ update: function($image) { this.image.hideResize(); this.buffer.set(); - var $link = $image.closest('a'); + var $link = $image.closest('a', this.$editor[0]); - $image.attr('alt', $('#redactor-image-title').val()); + var title = $('#redactor-image-title').val().replace(/(<([^>]+)>)/ig,""); + $image.attr('alt', title); this.image.setFloating($image); // as link var link = $.trim($('#redactor-image-link').val()); + var link = link.replace(/(<([^>]+)>)/ig,""); if (link !== '') { + // 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; + } + var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false; - if ($link.size() === 0) + if ($link.length === 0) { var a = $('<a href="' + link + '">' + this.utils.getOuterHtml($image) + '</a>'); if (target) a.attr('target', '_blank'); $image.replaceWith(a); @@ -3194,11 +3757,11 @@ { $link.removeAttr('target'); } } } - else if ($link.size() !== 0) + else if ($link.length !== 0) { $link.replaceWith(this.utils.getOuterHtml($image)); } @@ -3213,32 +3776,32 @@ if (this.opts.imageEditable) { $image.on('dragstart', $.proxy(this.image.onDrag, this)); } - $image.on('mousedown', $.proxy(this.image.hideResize, this)); - $image.on('click touchstart', $.proxy(function(e) + var handler = $.proxy(function(e) { + 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)); + $(document).on('mousedown.redactor-image-resize-hide.' + this.uuid, $.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); - }, this)); + + $image.off('mousedown.redactor').on('mousedown.redactor', $.proxy(this.image.hideResize, this)); + $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor', handler); }, setResizable: function(e, $image) { e.preventDefault(); @@ -3280,12 +3843,15 @@ var width = Math.round(height * this.image.resizeHandle.ratio); if (height < 50 || width < 100) return; + var height = Math.round(this.image.resizeHandle.el.width() / this.image.resizeHandle.ratio); + + this.image.resizeHandle.el.attr({width: width, height: height}); this.image.resizeHandle.el.width(width); - this.image.resizeHandle.el.height(this.image.resizeHandle.el.width()/this.image.resizeHandle.ratio); + this.image.resizeHandle.el.height(height); this.code.sync(); }, stopResize: function() { @@ -3294,11 +3860,11 @@ this.image.hideResize(); }, onDrag: function(e) { - if (this.$editor.find('#redactor-image-box').size() !== 0) + if (this.$editor.find('#redactor-image-box').length !== 0) { e.preventDefault(); return false; } @@ -3325,27 +3891,23 @@ $el.removeAttr('data-save-url'); }); }, hideResize: function(e) { - if (e && $(e.target).closest('#redactor-image-box').length !== 0) return; + if (e && $(e.target).closest('#redactor-image-box', this.$editor[0]).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 (imageBox.length === 0) return; - if (this.opts.imageEditable) - { - this.image.editter.remove(); - } + $('#redactor-image-editter').remove(); + $('#redactor-image-resizer').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 @@ -3356,13 +3918,13 @@ imageBox.replaceWith(function() { return $(this).contents(); }); - $(document).off('click.redactor-image-resize-hide'); - this.$editor.off('click.redactor-image-resize-hide'); + $(document).off('mousedown.redactor-image-resize-hide.' + this.uuid); + if (typeof this.image.resizeHandle !== 'undefined') { this.image.resizeHandle.el.attr('rel', this.image.resizeHandle.el.attr('style')); } @@ -3437,25 +3999,25 @@ }, remove: function(image) { var $image = $(image); - var $link = $image.closest('a'); - var $figure = $image.closest('figure'); + var $link = $image.closest('a', this.$editor[0]); + var $figure = $image.closest('figure', this.$editor[0]); var $parent = $image.parent(); - if ($('#redactor-image-box').size() !== 0) + if ($('#redactor-image-box').length !== 0) { $parent = $('#redactor-image-box').parent(); } var $next; - if ($figure.size() !== 0) + if ($figure.length !== 0) { $next = $figure.next(); $figure.remove(); } - else if ($link.size() !== 0) + else if ($link.length !== 0) { $parent = $link.parent(); $link.remove(); } else @@ -3463,11 +4025,11 @@ $image.remove(); } $('#redactor-image-box').remove(); - if ($figure.size() !== 0) + if ($figure.length !== 0) { this.caret.setStart($next); } else { @@ -3523,22 +4085,26 @@ } this.selection.restore(); this.buffer.set(); - 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 (!this.utils.isEmpty(this.code.get())) + { + $image.before('<br>'); + } + + $image.after('<br>'); } if (typeof json == 'string') return; this.core.setCallback('imageUpload', $image, json); @@ -3618,26 +4184,20 @@ } this.selection.restore(); this.code.sync(); }, - decreaseLists: function () + decreaseLists: function() { document.execCommand('outdent'); var current = this.selection.getCurrent(); + var $item = $(current).closest('li', this.$editor[0]); - var $item = $(current).closest('li'); - var $parent = $item.parent(); - if ($item.size() !== 0 && $parent.size() !== 0 && $parent[0].tagName == 'LI') - { - $parent.after($item); - } - this.indent.fixEmptyIndent(); - if (!this.opts.linebreaks && $item.size() === 0) + if (!this.opts.linebreaks && $item.length === 0) { document.execCommand('formatblock', false, 'p'); this.$editor.find('ul, ol, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); } @@ -3688,19 +4248,22 @@ formatting: function(name) { var type, value; if (typeof this.formatting[name].style != 'undefined') type = 'style'; - else if (typeof this.formatting[name].class != 'undefined') type = 'class'; + 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); }, format: function(tag, type, value) { + var current = this.selection.getCurrent(); + if (current && current.tagName === 'TR') return; + // 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']; @@ -3708,16 +4271,30 @@ for (var i = 0; i < tags.length; i++) { if (tag == tags[i]) tag = replaced[i]; } + + if (this.opts.allowedTags) + { + if ($.inArray(tag, this.opts.allowedTags) == -1) return; + } + else + { + if ($.inArray(tag, this.opts.deniedTags) !== -1) return; + } + this.inline.type = type || false; this.inline.value = value || false; this.buffer.set(); - this.$editor.focus(); + if (!this.utils.browser('msie') && !this.opts.linebreaks) + { + this.$editor.focus(); + } + this.selection.get(); if (this.range.collapsed) { this.inline.formatCollapsed(tag); @@ -3728,21 +4305,27 @@ } }, formatCollapsed: function(tag) { var current = this.selection.getCurrent(); - var $parent = $(current).closest(tag + '[data-redactor-tag=' + tag + ']'); + var $parent = $(current).closest(tag + '[data-redactor-tag=' + tag + ']', this.$editor[0]); // inline there is - if ($parent.size() !== 0) + if ($parent.length !== 0 && (this.inline.type != 'style' && $parent[0].tagName != 'SPAN')) { - this.caret.setAfter($parent[0]); - // remove empty - if (this.utils.isEmpty($parent.text())) $parent.remove(); + if (this.utils.isEmpty($parent.text())) + { + this.caret.setAfter($parent[0]); - this.code.sync(); + $parent.remove(); + this.code.sync(); + } + else if (this.utils.isEndOfElement($parent)) + { + this.caret.setAfter($parent[0]); + } return; } // create empty inline @@ -3761,11 +4344,10 @@ this.inline.formatConvert(tag); this.selection.save(); document.execCommand('strikethrough'); - this.$editor.find('strike').each($.proxy(function(i,s) { var $el = $(s); this.inline.formatRemoveSameChildren($el, tag); @@ -3780,15 +4362,21 @@ { $span = $('<' + tag + '>').attr('data-redactor-tag', tag).attr('data-verified', 'redactor'); } $el.replaceWith($span.html($el.contents())); + var $parent = $span.parent(); + // remove U tag if selected link + node + if ($span[0].tagName === 'A' && $parent && $parent[0].tagName === 'U') + { + $span.parent().replaceWith($span); + } + if (tag == 'span') { - var $parent = $span.parent(); - if ($parent && $parent[0].tagName == 'SPAN' && this.inline.type == 'style') + if ($parent && $parent[0].tagName === 'SPAN' && this.inline.type === 'style') { var arr = this.inline.value.split(';'); for (var z = 0; z < arr.length; z++) { @@ -3812,12 +4400,20 @@ if (tag != 'span') { this.$editor.find(this.opts.inlineTags.join(', ')).each($.proxy(function(i,s) { var $el = $(s); + + + if (s.tagName === 'U' && s.attributes.length === 0) + { + $el.replaceWith($el.contents()); + return; + } + var property = $el.css('text-decoration'); - if (property == 'line-through') + if (property === 'line-through') { $el.css('text-decoration', ''); this.utils.removeEmptyAttr($el, 'style'); } }, this)); @@ -3836,17 +4432,41 @@ this.code.sync(); }, formatRemoveSameChildren: function($el, tag) { + var self = this; $el.children(tag).each(function() { var $child = $(this); + if (!$child.hasClass('redactor-selection-marker')) { - $child.contents().unwrap(); + if (self.inline.type == 'style') + { + var arr = self.inline.value.split(';'); + + for (var z = 0; z < arr.length; z++) + { + if (arr[z] === '') return; + + var style = arr[z].split(':'); + $child.css(style[0], ''); + + if (self.utils.removeEmptyAttr($child , 'style')) + { + $child.replaceWith($child.contents()); + } + + } + } + else + { + $child.contents().unwrap(); + } } + }); }, formatConvert: function(tag) { this.selection.save(); @@ -3856,19 +4476,29 @@ else if (this.inline.type == 'style') { find = '[data-redactor-style="' + this.inline.value + '"]'; } + var self = this; if (tag != 'del') { - var self = this; this.$editor.find('del').each(function(i,s) { self.utils.replaceToTag(s, 'inline'); }); } + if (tag != 'span') + { + this.$editor.find(tag).each(function() + { + var $el = $(this); + $el.replaceWith($('<strike />').html($el.contents())); + + }); + } + this.$editor.find('[data-redactor-tag="' + tag + '"]' + find).each(function() { if (find === '' && tag == 'span' && this.tagName.toLowerCase() == tag) return; var $el = $(this); @@ -4094,11 +4724,14 @@ { this.placeholder.remove(); if (typeof clean == 'undefined') clean = true; - this.$editor.focus(); + if (!this.opts.linebreaks) + { + this.$editor.focus(); + } html = this.clean.setVerified(html); if (clean) { @@ -4192,11 +4825,14 @@ node = node[0] || node; var html = this.utils.getOuterHtml(node); html = this.clean.setVerified(html); - node = $(html)[0]; + if (html.match(/</g) !== null) + { + node = $(html)[0]; + } this.selection.get(); if (deleteContents !== false) { @@ -4277,10 +4913,11 @@ this.insert.node(node); var parHtml = $(parent).html(); parHtml = '<p>' + parHtml.replace(/<span class="redactor-ie-paste"><\/span>/gi, '</p>' + html + '<p>') + '</p>'; + parHtml = parHtml.replace(/<p><\/p>/gi, ''); $(parent).replaceWith(parHtml); }, ie11PasteFrag: function(html) { this.selection.get(); @@ -4294,10 +4931,12 @@ { lastNode = frag.appendChild(node); } this.range.insertNode(frag); + this.range.collapse(false); + this.selection.addRange(); } }; }, keydown: function() { @@ -4320,12 +4959,16 @@ this.keydown.figcaption = this.utils.isTag(this.keydown.current, 'figcaption'); // shortcuts setup this.shortcuts.init(e, key); - this.keydown.checkEvents(arrow, key); - this.keydown.setupBuffer(e, key); + if (this.utils.isDesktop()) + { + this.keydown.checkEvents(arrow, key); + this.keydown.setupBuffer(e, key); + } + this.keydown.addArrowsEvent(arrow); this.keydown.setupSelectAll(e, key); // callback var keydownStop = this.core.setCallback('keydown', e); @@ -4340,11 +4983,11 @@ { var isEndOfTable = false; var $table = false; if (this.keydown.block && this.keydown.block.tagName === 'TD') { - $table = $(this.keydown.block).closest('table'); + $table = $(this.keydown.block).closest('table', this.$editor[0]); } if ($table && $table.find('td').last()[0] === this.keydown.block) { isEndOfTable = true; @@ -4396,11 +5039,11 @@ else if (this.keydown.blockquote || this.keydown.figcaption) { current = this.selection.getCurrent(); $next = $(current).next(); - if ($next.size() !== 0 && $next[0].tagName == 'BR') + if ($next.length !== 0 && $next[0].tagName == 'BR') { return this.keydown.insertBreakLine(e); } else if (this.utils.isEndOfElement() && (current && current != 'SPAN')) { @@ -4414,43 +5057,69 @@ else if (this.opts.linebreaks && !this.keydown.block) { current = this.selection.getCurrent(); $next = $(this.keydown.current).next(); - if (current !== false && $(current).hasClass('redactor-invisible-space')) + if ($next.length !== 0 && $next[0].tagName == 'BR') { - $(current).remove(); + return this.keydown.insertBreakLine(e); + } + else if (current !== false && $(current).hasClass('redactor-invisible-space')) + { + this.caret.setAfter(current); + $(current).contents().unwrap(); + return this.keydown.insertDblBreakLine(e); } else { - if ($next.length === 0 && current === false && typeof $next.context != 'undefined') + if (this.utils.isEndOfEditor()) { return this.keydown.insertDblBreakLine(e); } + else if ($next.length === 0 && current === false && typeof $next.context != 'undefined') + { + return this.keydown.insertBreakLine(e); + } return this.keydown.insertBreakLine(e); } + } else if (this.opts.linebreaks && this.keydown.block) { setTimeout($.proxy(this.keydown.replaceDivToBreakLine, this), 1); } // paragraphs - else if (!this.opts.linebreaks && this.keydown.block && this.keydown.block.tagName !== 'LI') + else if (!this.opts.linebreaks && this.keydown.block) { setTimeout($.proxy(this.keydown.replaceDivToParagraph, this), 1); + + if (this.keydown.block.tagName === 'LI') + { + current = this.selection.getCurrent(); + var $parent = $(current).closest('li', this.$editor[0]); + var $list = $parent.closest('ul,ol', this.$editor[0]); + + if ($parent.length !== 0 && this.utils.isEmpty($parent.html()) && $list.next().length === 0 && this.utils.isEmpty($list.find("li").last().html())) + { + $list.find("li").last().remove(); + + var node = $(this.opts.emptyHtml); + $list.after(node); + this.caret.setStart(node); + + return false; + } + } } 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)) { return this.keydown.onShiftEnter(e); } @@ -4460,23 +5129,23 @@ if (key === this.keyCode.TAB || e.metaKey && key === 221 || e.metaKey && key === 219) { return this.keydown.onTab(e, key); } - // image delete and backspace if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) { var nodes = this.selection.getNodes(); + if (nodes) { var len = nodes.length; var last; for (var i = 0; i < len; i++) { var children = $(nodes[i]).children('img'); - if (children.size() !== 0) + if (children.length !== 0) { var self = this; $.each(children, function(z,s) { var $s = $(s); @@ -4501,10 +5170,29 @@ } // backspace if (key === this.keyCode.BACKSPACE) { + // backspace as outdent + var block = this.selection.getBlock(); + var indented = ($(block).css('margin-left') !== '0px'); + if (block && indented && this.range.collapsed && this.utils.isStartOfElement()) + { + this.indent.decrease(); + e.preventDefault(); + return; + } + + // remove hr in FF + if (this.utils.browser('mozilla')) + { + var prev = this.selection.getPrev(); + var prev2 = $(prev).prev()[0]; + if (prev && prev.tagName === 'HR') $(prev).remove(); + if (prev2 && prev2.tagName === 'HR') $(prev2).remove(); + } + this.keydown.removeInvisibleSpace(); this.keydown.removeEmptyListInTable(e); } this.code.sync(); @@ -4522,11 +5210,11 @@ } }, checkKeyEvents: function(key) { var k = this.keyCode; - var keys = [k.BACKSPACE, k.DELETE, k.ENTER, k.SPACE, k.ESC, k.TAB, k.CTRL, k.META, k.ALT, k.SHIFT]; + var keys = [k.BACKSPACE, k.DELETE, k.ENTER, k.ESC, k.TAB, k.CTRL, k.META, k.ALT, k.SHIFT]; return ($.inArray(key, keys) == -1) ? true : false; }, addArrowsEvent: function(arrow) @@ -4556,11 +5244,11 @@ this.buffer.redo(); return; } else if (!this.keydown.ctrl) { - if (key == this.keyCode.BACKSPACE || key == this.keyCode.DELETE || (key == this.keyCode.ENTER && !e.ctrlKey && !e.shiftKey) || key == this.keyCode.SPACE) + if (key == this.keyCode.BACKSPACE || key == this.keyCode.DELETE || (key == this.keyCode.ENTER && !e.ctrlKey && !e.shiftKey)) { this.buffer.set(); } } }, @@ -4647,11 +5335,11 @@ }, replaceDivToParagraph: function() { var blockElem = this.selection.getBlock(); var blockHtml = blockElem.innerHTML.replace(/<br\s?\/?>/gi, ''); - if (blockElem.tagName === 'DIV' && blockHtml === '' && !$(blockElem).hasClass('redactor-editor')) + if (blockElem.tagName === 'DIV' && this.utils.isEmpty(blockHtml) && !$(blockElem).hasClass('redactor-editor')) { var p = document.createElement('p'); p.innerHTML = this.opts.invisibleSpace; $(blockElem).replaceWith(p); @@ -4774,28 +5462,74 @@ insertBreakLineProcessing: function(e, dbl) { e.stopPropagation(); this.selection.get(); + var br1 = document.createElement('br'); - this.range.deleteContents(); + if (this.utils.browser('msie')) + { + this.range.collapse(false); + this.range.setEnd(this.range.endContainer, this.range.endOffset); + } + else + { + this.range.deleteContents(); + } + this.range.insertNode(br1); + // move br outside A tag + var $parentA = $(br1).parent("a"); + + if ($parentA.length > 0) + { + $parentA.find(br1).remove(); + $parentA.after(br1); + } + if (dbl === true) { + var $next = $(br1).next(); + if ($next.length !== 0 && $next[0].tagName === 'BR' && this.utils.isEndOfEditor()) + { + this.caret.setAfter(br1); + this.code.sync(); + return false; + } + var br2 = document.createElement('br'); + this.range.insertNode(br2); this.caret.setAfter(br2); } else { - this.caret.setAfter(br1); + // caret does not move after the br visual + if (this.utils.browser('msie')) + { + var space = document.createElement('span'); + space.innerHTML = '&#x200b;'; + + $(br1).after(space); + this.caret.setAfter(space); + $(space).remove(); + } + else + { + var range = document.createRange(); + range.setStartAfter(br1); + range.collapse(true); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + + } } this.code.sync(); - return false; }, removeInvisibleSpace: function() { var $current = $(this.keydown.current); @@ -4806,13 +5540,13 @@ }, removeEmptyListInTable: function(e) { var $current = $(this.keydown.current); var $parent = $(this.keydown.parent); - var td = $current.closest('td'); + var td = $current.closest('td', this.$editor[0]); - if (td.size() !== 0 && $current.closest('li') && $parent.children('li').size() === 1) + if (td.length !== 0 && $current.closest('li', this.$editor[0]) && $parent.children('li').length === 1) { if (!this.utils.isEmpty($current.text())) return; e.preventDefault(); @@ -4827,29 +5561,29 @@ keyup: function() { return { init: function(e) { + if (this.rtePaste) return; var key = e.which; 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(); return false; } // replace to p before / after the table or body - if (!this.opts.linebreaks && this.keyup.current.nodeType == 3 && this.keyup.current.length <= 1 && (this.keyup.parent === false || this.keyup.parent.tagName == 'BODY')) + 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(); } // replace div after lists @@ -4864,20 +5598,24 @@ $(this.keyup.parent).contents().unwrap(); this.keyup.replaceToParagraph(); } // linkify - 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); + if (this.linkify.isEnabled() && this.linkify.isKey(key)) this.linkify.format(); - this.observe.load(); - this.code.sync(); - } - if (key === this.keyCode.DELETE || key === this.keyCode.BACKSPACE) { + if (this.utils.browser('mozilla')) + { + var td = $(this.keydown.current).closest('td', this.$editor[0]); + if (td.size() !== 0 && td.text() !== '') + { + e.preventDefault(); + return false; + } + } + // clear unverified this.clean.clearUnverified(); if (this.observe.image) { @@ -4891,31 +5629,27 @@ return false; } // remove empty paragraphs - this.$editor.find('p').each($.proxy(this.utils.removeEmpty, this)); + this.$editor.find('p').each($.proxy(function(i, s) + { + this.utils.removeEmpty(i, $(s).html()); + }, this)); // remove invisible space - if (this.keyup.current && this.keyup.current.tagName == 'DIV' && this.utils.isEmpty(this.keyup.current.innerHTML)) + if (this.opts.linebreaks && this.keyup.current && this.keyup.current.tagName == 'DIV' && this.utils.isEmpty(this.keyup.current.innerHTML)) { - if (this.opts.linebreaks) - { - $(this.keyup.current).after(this.selection.getMarkerAsHtml()); - this.selection.restore(); - $(this.keyup.current).remove(); - } + $(this.keyup.current).after(this.selection.getMarkerAsHtml()); + this.selection.restore(); + $(this.keyup.current).remove(); } // if empty return this.keyup.formatEmpty(e); } }, - 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; @@ -4950,13 +5684,11 @@ this.$editor.html(this.selection.getMarkerAsHtml()); this.selection.restore(); } else { - html = '<p><br /></p>'; - - this.$editor.html(html); + this.$editor.html(this.opts.emptyHtml); this.focus.setStart(); } this.code.sync(); @@ -5032,44 +5764,81 @@ this.buffer.set(); var extra = '<p id="redactor-insert-line"><br /></p>'; if (this.opts.linebreaks) extra = '<br id="redactor-insert-line">'; - document.execCommand('insertHTML', false, '<hr>' + extra); + document.execCommand('insertHtml', false, '<hr>' + extra); this.line.setFocus(); this.code.sync(); }, setFocus: function() { var node = this.$editor.find('#redactor-insert-line'); var next = $(node).next()[0]; + var target = next; + if (this.utils.browser('mozilla') && next && next.innerHTML === '') + { + target = $(next).next()[0]; + $(next).remove(); + } - if (next) + if (target) { - this.caret.setAfter(node); node.remove(); + + if (!this.opts.linebreaks) + { + this.$editor.focus(); + this.line.setStart(target); + } + } else { + node.removeAttr('id'); + this.line.setStart(node[0]); } + }, + setStart: function(node) + { + if (typeof node === 'undefined') return; + + var textNode = document.createTextNode('\u200B'); + + this.selection.get(); + this.range.setStart(node, 0); + this.range.insertNode(textNode); + this.range.collapse(true); + this.selection.addRange(); + } }; }, link: function() { return { show: function(e) { if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); - this.modal.load('link', this.lang.get('link_insert'), 600); + if (!this.observe.isCurrent('a')) + { + this.modal.load('link', this.lang.get('link_insert'), 600); + } + else + { + this.modal.load('link', this.lang.get('link_edit'), 600); + } this.modal.createCancelButton(); - this.link.buttonInsert = this.modal.createActionButton(this.lang.get('insert')); + var buttonText = !this.observe.isCurrent('a') ? this.lang.get('insert') : this.lang.get('edit'); + + this.link.buttonInsert = this.modal.createActionButton(buttonText); + this.selection.get(); this.link.getData(); this.link.cleanUrl(); @@ -5092,28 +5861,31 @@ 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) + if (typeof this.link.url !== "undefined") { - var re = new RegExp('^(http|ftp|https)://' + self.location.host, 'i'); - this.link.url = this.link.url.replace(re, ''); - } + 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') + var $el = $(this.selection.getCurrent()).closest('a', this.$editor[0]); + if ($el.length !== 0 && $el[0].tagName === 'A') { this.link.$node = $el; this.link.url = $el.attr('href'); this.link.text = $el.text(); @@ -5127,13 +5899,15 @@ } }, insert: function() { + this.placeholder.remove(); + var target = ''; var link = this.link.$inputUrl.val(); - var text = this.link.$inputText.val(); + var text = this.link.$inputText.val().replace(/(<([^>]+)>)/ig,""); if ($.trim(link) === '') { this.link.$inputUrl.addClass('redactor-input-error').on('keyup', function() { @@ -5160,12 +5934,12 @@ // 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) + var re3 = new RegExp('\.(html|php)$', 'i'); + if (link.search(re) == -1 && link.search(re3) == -1 && link.search(re2) === 0 && this.opts.linkProtocol) { link = this.opts.linkProtocol + '://' + link; } } @@ -5175,38 +5949,66 @@ set: function(text, link, target) { text = $.trim(text.replace(/<|>/g, '')); this.selection.restore(); + var blocks = this.selection.getBlocks(); if (text === '' && link === '') return; if (text === '' && link !== '') text = link; if (this.link.$node) { this.buffer.set(); - this.link.$node.text(text).attr('href', link); + var $link = this.link.$node, + $el = $link.children(); + + if ($el.length > 0) + { + while ($el.length) + { + $el = $el.children(); + } + + $el = $el.end(); + } + else + { + $el = $link; + } + + $link.attr('href', link); + $el.text(text); + if (target !== '') { - this.link.$node.attr('target', target); + $link.attr('target', target); } else { - this.link.$node.removeAttr('target'); + $link.removeAttr('target'); } + this.selection.selectElement($link); + 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); + $a = $(this.insert.node($a)); + + if (this.opts.linebreaks) + { + $a.after('&nbsp;'); + } + this.selection.selectElement($a); } else { var $a; @@ -5214,24 +6016,47 @@ { $a = $('<a href="' + link + '">').text(text); if (target !== '') $a.attr('target', target); $a = $(this.insert.node($a)); + + if (this.selection.getText().match(/\s$/)) + { + $a.after(" "); + } + this.selection.selectElement($a); } else { document.execCommand('createLink', false, link); - $a = $(this.selection.getCurrent()).closest('a'); + $a = $(this.selection.getCurrent()).closest('a', this.$editor[0]); + if (this.utils.browser('mozilla')) + { + $a = $('a[_moz_dirty=""]'); + } if (target !== '') $a.attr('target', target); - $a.removeAttr('style'); + $a.removeAttr('style').removeAttr('_moz_dirty'); - if (this.link.text === '') + if (this.selection.getText().match(/\s$/)) { - $a.text(text); + $a.after(" "); + } + + if (this.link.text !== '' || this.link.text != text) + { + if (!this.opts.linebreaks && blocks && blocks.length <= 1) + { + $a.text(text); + } + else if (this.opts.linebreaks) + { + $a.text(text); + } + this.selection.selectElement($a); } } } @@ -5247,35 +6072,219 @@ }, this), 5); }, unlink: function(e) { - if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); + if (typeof e != 'undefined' && e.preventDefault) + { + e.preventDefault(); + } var nodes = this.selection.getNodes(); if (!nodes) return; this.buffer.set(); var len = nodes.length; + var links = []; for (var i = 0; i < len; i++) { - if (nodes[i].tagName == 'A') + if (nodes[i].tagName === 'A') { - var $node = $(nodes[i]); - $node.replaceWith($node.contents()); + links.push(nodes[i]); } + + var $node = $(nodes[i]).closest('a', this.$editor[0]); + $node.replaceWith($node.contents()); } + this.core.setCallback('deletedLink', links); + // hide link's tooltip $('.redactor-link-tooltip').remove(); this.code.sync(); + }, + toggleClass: function(className) + { + this.link.setClass(className, 'toggleClass'); + }, + addClass: function(className) + { + this.link.setClass(className, 'addClass'); + }, + removeClass: function(className) + { + this.link.setClass(className, 'removeClass'); + }, + setClass: function(className, func) + { + var links = this.selection.getInlinesTags(['a']); + if (links === false) return; + + $.each(links, function() + { + $(this)[func](className); + }); } }; }, + linkify: function() + { + return { + isKey: function(key) + { + return key == this.keyCode.ENTER || key == this.keyCode.SPACE; + }, + isEnabled: function() + { + return this.opts.convertLinks && (this.opts.convertUrlLinks || this.opts.convertImageLinks || this.opts.convertVideoLinks) && !this.utils.isCurrentOrParent('PRE'); + }, + format: function() + { + var linkify = this.linkify, + opts = this.opts; + + this.$editor + .find(":not(iframe,img,a,pre)") + .addBack() + .contents() + .filter(function() + { + return this.nodeType === 3 && $.trim(this.nodeValue) != "" && !$(this).parent().is("pre") && (this.nodeValue.match(opts.linkify.regexps.youtube) || this.nodeValue.match(opts.linkify.regexps.vimeo) || this.nodeValue.match(opts.linkify.regexps.image) || this.nodeValue.match(opts.linkify.regexps.url)); + }) + .each(function() + { + var text = $(this).text(), + html = text; + + if (opts.convertVideoLinks && (html.match(opts.linkify.regexps.youtube) || html.match(opts.linkify.regexps.vimeo)) ) + { + html = linkify.convertVideoLinks(html); + } + else if (opts.convertImageLinks && html.match(opts.linkify.regexps.image)) + { + html = linkify.convertImages(html); + } + else if (opts.convertUrlLinks) + { + html = linkify.convertLinks(html); + } + + $(this).before(text.replace(text, html)) + .remove(); + }); + + + var objects = this.$editor.find('.redactor-linkify-object').each(function() + { + var $el = $(this); + $el.removeClass('redactor-linkify-object'); + if ($el.attr('class') === '') $el.removeAttr('class'); + + return $el[0]; + + }); + + // callback + setTimeout($.proxy(function() + { + this.observe.load(); + this.core.setCallback('linkify', objects); + }, this), 100); + + // sync + this.code.sync(); + }, + convertVideoLinks: function(html) + { + var iframeStart = '<iframe class="redactor-linkify-object" width="500" height="281" src="', + iframeEnd = '" frameborder="0" allowfullscreen></iframe>'; + + if (html.match(this.opts.linkify.regexps.youtube)) + { + html = html.replace(this.opts.linkify.regexps.youtube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd); + } + + if (html.match(this.opts.linkify.regexps.vimeo)) + { + html = html.replace(this.opts.linkify.regexps.vimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd); + } + + return html; + }, + convertImages: function(html) + { + var matches = html.match(this.opts.linkify.regexps.image); + + if (matches) + { + html = html.replace(html, '<img src="' + matches + '" class="redactor-linkify-object" />'); + + if (this.opts.linebreaks) + { + if (!this.utils.isEmpty(this.code.get())) + { + html = '<br>' + html; + } + } + + html += '<br>'; + } + + return html; + }, + convertLinks: function(html) + { + var matches = html.match(this.opts.linkify.regexps.url); + + if (matches) + { + matches = $.grep(matches, function(v, k) { return $.inArray(v, matches) === k; }); + + var length = matches.length; + + for (var i = 0; i < length; i++) + { + var href = matches[i], + text = href, + linkProtocol = this.opts.linkProtocol + '://'; + + if (href.match(/(https?|ftp):\/\//i) !== null) + { + linkProtocol = ""; + } + + if (text.length > this.opts.linkSize) + { + text = text.substring(0, this.opts.linkSize) + '...'; + } + + if (text.search('%') === -1) + { + text = decodeURIComponent(text); + } + + var regexB = "\\b"; + + if ($.inArray(href.slice(-1), ["/", "&", "="]) != -1) + { + regexB = ""; + } + + // escaping url + var regexp = new RegExp('(' + href.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") + regexB + ')', 'g'); + + html = html.replace(regexp, '<a href="' + linkProtocol + $.trim(href) + '" class="redactor-linkify-object">' + $.trim(text) + '</a>'); + } + } + + return html; + } + }; + }, list: function() { return { toggle: function(cmd) { @@ -5284,13 +6293,13 @@ this.buffer.set(); this.selection.save(); var parent = this.selection.getParent(); - var $list = $(parent).closest('ol, ul'); + var $list = $(parent).closest('ol, ul', this.$editor[0]); - if (!this.utils.isRedactorParent($list) && $list.size() !== 0) + if (!this.utils.isRedactorParent($list) && $list.length !== 0) { $list = false; } var isUnorderedCmdOrdered, isOrderedCmdUnordered; @@ -5314,61 +6323,55 @@ } else { if (remove) { - this.list.remove(cmd); + this.list.remove(cmd, $list); } else { this.list.insert(cmd); } } - 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'); + var $td = $(current).closest('td, th', this.$editor[0]); if (this.utils.browser('msie') && this.opts.linebreaks) { this.list.insertInIe(cmd); } else { document.execCommand('insert' + cmd); } - var $list = $(this.selection.getParent()).closest('ol, ul'); - - if ($td.size() !== 0) + var parent = this.selection.getParent(); + var $list = $(parent).closest('ol, ul', this.$editor[0]); + if ($td.length !== 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); + var newTd = $td.clone(); + $td.after(newTd).remove(''); } + if (this.utils.isEmpty($list.find('li').text())) { var $children = $list.children('li'); $children.find('br').remove(); $children.append(this.selection.getMarkerAsHtml()); + + if (this.opts.linebreaks && this.utils.browser('mozilla') && $children.size() == 2 && this.utils.isEmpty($children.eq(1).text())) + { + $children.eq(1).remove(); + } } if ($list.length) { // remove block-element list wrapper @@ -5382,10 +6385,11 @@ if (!this.utils.browser('msie')) { this.$editor.focus(); } + this.clean.clearUnverified(); }, insertInIe: function(cmd) { var wrapper = this.selection.wrap('div'); @@ -5420,33 +6424,35 @@ } $(wrapper).replaceWith(tmpList); } }, - remove: function(cmd) + remove: function(cmd, $list) { + if ($.inArray('ul', this.selection.getBlocks())) cmd = 'unorderedlist'; + document.execCommand('insert' + cmd); var $current = $(this.selection.getCurrent()); - this.indent.fixEmptyIndent(); - if (!this.opts.linebreaks && $current.closest('li, th, td').size() === 0) + if (!this.opts.linebreaks && $current.closest('li, th, td', this.$editor[0]).length === 0) { document.execCommand('formatblock', false, 'p'); this.$editor.find('ul, ol, blockquote').each($.proxy(this.utils.removeEmpty, this)); } - var $table = $(this.selection.getCurrent()).closest('table'); + var $table = $(this.selection.getCurrent()).closest('table', this.$editor[0]); var $prev = $table.prev(); - if (!this.opts.linebreaks && $table.size() !== 0 && $prev.size() !== 0 && $prev[0].tagName == 'BR') + if (!this.opts.linebreaks && $table.length !== 0 && $prev.length !== 0 && $prev[0].tagName == 'BR') { $prev.remove(); } this.clean.clearUnverified(); + } }; }, modal: function() { @@ -5458,14 +6464,14 @@ 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>' + + '<input type="text" id="redactor-image-link" class="redactor-image-link-option" aria-label="' + this.lang.get('link') + '" />' + + '<label class="redactor-image-link-option"><input type="checkbox" id="redactor-image-link-blank" aria-label="' + this.lang.get('link_new_tab') + '"> ' + 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">' + + '<select class="redactor-image-position-option" id="redactor-image-align" aria-label="' + this.lang.get('image_position') + '">' + '<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>' @@ -5478,21 +6484,21 @@ 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>' + + '<input type="text" id="redactor-filename" aria-label="' + this.lang.get('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" />' + + '<input type="url" id="redactor-link-url" aria-label="URL" />' + '<label>' + this.lang.get('text') + '</label>' - + '<input type="text" id="redactor-link-url-text" />' + + '<input type="text" id="redactor-link-url-text" aria-label="' + this.lang.get('text') + '" />' + '<label><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get('link_new_tab') + '</label>' + '</section>' }; @@ -5561,32 +6567,32 @@ } }, show: function() { - // ios keyboard hide - if (this.utils.isMobile() && !this.utils.browser('msie')) - { - document.activeElement.blur(); - } + this.utils.disableBodyScroll(); - $(document.body).removeClass('body-redactor-hidden'); - this.modal.bodyOveflow = $(document.body).css('overflow'); - $(document.body).css('overflow', 'hidden'); - if (this.utils.isMobile()) { this.modal.showOnMobile(); } else { this.modal.showOnDesktop(); } + if (this.opts.highContrast) + { + this.$modalBox.addClass("redactor-modal-contrast"); + } + this.$modalOverlay.show(); this.$modalBox.show(); + this.$modal.attr('tabindex', '-1'); + this.$modal.focus(); + this.modal.setButtonsWidth(); this.utils.saveScroll(); // resize @@ -5601,12 +6607,11 @@ // fix bootstrap modal focus $(document).off('focusin.modal'); // enter - this.$modal.find('input[type=text]').on('keydown.redactor-modal', $.proxy(this.modal.setEnter, this)); - + this.$modal.find('input[type=text],input[type=url],input[type=email]').on('keydown.redactor-modal', $.proxy(this.modal.setEnter, this)); }, showOnDesktop: function() { var height = this.$modal.outerHeight(); var windowHeight = $(window).height(); @@ -5700,23 +6705,23 @@ return button; }, setButtonsWidth: function() { var buttons = this.$modalFooter.find('button'); - var buttonsSize = buttons.size(); + var buttonsSize = buttons.length; 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.$modalBox = $('<div id="redactor-modal-box"/>').hide(); + this.$modal = $('<div id="redactor-modal" role="dialog" aria-labelledby="redactor-modal-header" />'); + this.$modalHeader = $('<header id="redactor-modal-header"/>'); + this.$modalClose = $('<button type="button" id="redactor-modal-close" tabindex="1" aria-label="Close" />').html('&times;'); this.$modalBody = $('<div id="redactor-modal-body" />'); this.$modalFooter = $('<footer />'); this.$modal.append(this.$modalHeader); this.$modal.append(this.$modalClose); @@ -5764,10 +6769,11 @@ } if (!this.$modalBox) return; this.modal.disableEvents(); + this.utils.enableBodyScroll(); this.$modalOverlay.remove(); this.$modalBox.fadeOut('fast', $.proxy(function() { @@ -5788,19 +6794,133 @@ observe: function() { return { load: function() { + if (typeof this.opts.destroyed != "undefined") return; + + if (this.utils.browser('msie')) + { + var self = this; + this.$editor.find('pre, code').on('mouseover',function() + { + self.$editor.attr('contenteditable', false); + $(this).attr('contenteditable', true); + + }).on('mouseout',function() + { + self.$editor.attr('contenteditable', true); + $(this).removeAttr('contenteditable'); + + }); + } + this.observe.images(); this.observe.links(); }, + toolbar: function(e, btnName) + { + this.observe.buttons(e, btnName); + this.observe.dropdowns(); + }, + isCurrent: function($el, $current) + { + if (typeof $current == 'undefined') + { + var $current = $(this.selection.getCurrent()); + } + + return $current.is($el) || $current.parents($el).length > 0; + }, + dropdowns: function() + { + var $current = $(this.selection.getCurrent()); + + $.each(this.opts.observe.dropdowns, $.proxy(function(key, value) + { + var observe = value.observe, + element = observe.element, + $item = value.item, + inValues = typeof observe['in'] != 'undefined' ? observe['in'] : false, + outValues = typeof observe['out'] != 'undefined' ? observe['out'] : false; + + if ($current.closest(element).size() > 0) + { + this.observe.setDropdownProperties($item, inValues, outValues); + } + else + { + this.observe.setDropdownProperties($item, outValues, inValues); + } + }, this)); + }, + setDropdownProperties: function($item, addProperties, deleteProperties) + { + if (deleteProperties && typeof deleteProperties['attr'] != 'undefined') + { + this.observe.setDropdownAttr($item, deleteProperties.attr, true); + } + + if (typeof addProperties['attr'] != 'undefined') + { + this.observe.setDropdownAttr($item, addProperties.attr); + } + + if (typeof addProperties['title'] != 'undefined') + { + $item.text(addProperties['title']); + } + }, + setDropdownAttr: function($item, properties, isDelete) + { + $.each(properties, function(key, value) + { + if (key == 'class') + { + if (!isDelete) + { + $item.addClass(value); + } + else + { + $item.removeClass(value); + } + } + else + { + if (!isDelete) + { + $item.attr(key, value); + } + else + { + $item.removeAttr(key); + } + } + }); + }, + addDropdown: function($item, btnName, btnObject) + { + if (typeof btnObject.observe == "undefined") return; + + btnObject.item = $item; + + this.opts.observe.dropdowns.push(btnObject); + }, buttons: function(e, btnName) { var current = this.selection.getCurrent(); var parent = this.selection.getParent(); - this.button.setInactiveAll(btnName); + if (e !== false) + { + this.button.setInactiveAll(); + } + else + { + this.button.setInactiveAll(btnName); + } if (e === false && btnName !== 'html') { if ($.inArray(btnName, this.opts.activeButtons) != -1) this.button.toggleActive(btnName); return; @@ -5809,23 +6929,23 @@ //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) { - var parentEl = $(parent).closest(key); - var currentEl = $(current).closest(key); + var parentEl = $(parent).closest(key, this.$editor[0]); + var currentEl = $(current).closest(key, this.$editor[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) + if (parentEl.length !== 0 || currentEl.closest(key, this.$editor[0]).length !== 0) { this.button.setActive(value); } }, this)); - var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase()); + var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]); if (this.utils.isRedactorParent(parent) && $parent.length) { var align = ($parent.css('text-align') === '') ? 'left' : $parent.css('text-align'); this.button.setActive('align' + align); } @@ -5840,19 +6960,19 @@ this.$editor.find('img').each($.proxy(function(i, img) { var $img = $(img); // 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(); }); + $img.closest('a', this.$editor[0]).on('click', function(e) { e.preventDefault(); }); if (this.utils.browser('msie')) $img.attr('unselectable', 'on'); this.image.setEditable($img); }, this)); - $(document).on('click.redactor-image-delete', $.proxy(function(e) + $(document).on('click.redactor-image-delete.' + this.uuid, $.proxy(function(e) { this.observe.image = false; if (e.target.tagName == 'IMG' && this.utils.isRedactorParent(e.target)) { this.observe.image = (this.observe.image && this.observe.image == e.target) ? false : e.target; @@ -5863,35 +6983,33 @@ }, 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)); + this.$editor.find('a').on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.showTooltip, this)); + this.$editor.on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this)); + $(document).on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this)); }, getTooltipPosition: function($link) { return $link.offset(); }, showTooltip: function(e) { - var $link = $(e.target); - var $parent = $link.closest('a'); - var tag = ($link.size() !== 0) ? $link[0].tagName : false; + var $el = $(e.target); - if ($parent[0].tagName === 'A') - { - if (tag === 'IMG') return; - else if (tag !== 'A') $link = $parent; - } + if ($el[0].tagName == 'IMG') + return; - if (tag !== 'A') - { + if ($el[0].tagName !== 'A') + $el = $el.closest('a', this.$editor[0]); + + if ($el[0].tagName !== 'A') return; - } + var $link = $el; + var pos = this.observe.getTooltipPosition($link); var tooltip = $('<span class="redactor-link-tooltip"></span>'); var href = $link.attr('href'); if (href === undefined) @@ -5905,11 +7023,11 @@ 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', + top: (pos.top + parseInt($link.css('line-height'), 10)) + 'px', left: pos.left + 'px' }); $('.redactor-link-tooltip').remove(); $('body').append(tooltip); @@ -5917,12 +7035,12 @@ closeTooltip: function(e) { e = e.originalEvent || e; var target = e.target; - var $parent = $(target).closest('a'); - if ($parent.size() !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A') + var $parent = $(target).closest('a', this.$editor[0]); + if ($parent.length !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A') { return; } else if ((target.tagName === 'A' && this.utils.isRedactorParent(target)) || $(target).hasClass('redactor-link-tooltip-action')) { @@ -5940,14 +7058,10 @@ load: function(html) { 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; @@ -5958,11 +7072,11 @@ 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>'); + html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts.paragraphizeBlocks.join('|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>'); return $.trim(html); }, getSafes: function(html) { @@ -5974,11 +7088,11 @@ return $(this).append('<br />').contents(); }); html = $div.html(); - $div.find(this.paragraphize.blocks.join(', ')).each($.proxy(function(i,s) + $div.find(this.opts.paragraphizeBlocks.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 + '}'); @@ -6003,11 +7117,13 @@ }, restoreSafes: function(html) { $.each(this.paragraphize.safes, function(i,s) { + s = (typeof s !== 'undefined') ? s.replace(/\$/g, '&#36;') : s; html = html.replace('{replace' + i + '}', s); + }); return html; }, replaceBreaksToParagraphs: function(html) @@ -6071,11 +7187,15 @@ paste: function() { return { init: function(e) { - if (!this.opts.cleanOnPaste) return; + if (!this.opts.cleanOnPaste) + { + setTimeout($.proxy(this.code.sync, this), 1); + return; + } this.rtePaste = true; this.buffer.set(); this.selection.save(); @@ -6100,18 +7220,40 @@ this.paste.insert(html); $(window).off('scroll.redactor-freeze'); - }, this), 1); + if (this.linkify.isEnabled()) + { + this.linkify.format(); + } + }, this), 1); }, createPasteBox: function() { this.$pasteBox = $('<div>').html('').attr('contenteditable', 'true').css({ position: 'fixed', width: 0, top: 0, left: '-9999px' }); - this.$box.parent().append(this.$pasteBox); + if (this.utils.browser('msie')) + { + this.$box.append(this.$pasteBox); + } + else + { + // bootstrap modal + if ($('.modal-body').length > 0) + { + + $('.modal.in .modal-body').append(this.$pasteBox); + } + else + { + $('body').append(this.$pasteBox); + } + + } + this.$pasteBox.focus(); }, insert: function(html) { html = this.core.setCallback('pasteBefore', html); @@ -6139,16 +7281,17 @@ setTimeout($.proxy(function() { var spans = this.$editor.find('span'); $.each(spans, function(i,s) { - var html = s.innerHTML.replace(/[\u200B-\u200D\uFEFF]/, ''); + var html = s.innerHTML.replace(/\u200B/, ''); if (html === '' && s.attributes.length === 0) $(s).remove(); }); }, this), 10); + } }; }, placeholder: function() { @@ -6158,18 +7301,20 @@ if (!this.placeholder.is()) return; this.$editor.attr('placeholder', this.$element.attr('placeholder')); this.placeholder.toggle(); - this.$editor.on('keyup.redactor-placeholder', $.proxy(this.placeholder.toggle, this)); - + this.$editor.on('keydown.redactor-placeholder', $.proxy(this.placeholder.toggle, this)); }, toggle: function() { - var func = 'removeClass'; - if (this.utils.isEmpty(this.$editor.html(), false)) func = 'addClass'; - this.$editor[func]('redactor-placeholder'); + setTimeout($.proxy(function() + { + var func = this.utils.isEmpty(this.$editor.html(), false) ? 'addClass' : 'removeClass'; + this.$editor[func]('redactor-placeholder'); + + }, this), 5); }, remove: function() { this.$editor.removeClass('redactor-placeholder'); }, @@ -6229,10 +7374,11 @@ this.sel.addRange(this.range); }, getCurrent: function() { var el = false; + this.selection.get(); if (this.sel && this.sel.rangeCount > 0) { el = this.sel.getRangeAt(0).startContainer; @@ -6248,10 +7394,18 @@ return this.utils.isRedactorParent($(elem).parent()[0]); } return false; }, + getPrev: function() + { + return window.getSelection().anchorNode.previousSibling; + }, + getNext: function() + { + return window.getSelection().anchorNode.nextSibling; + }, getBlock: function(node) { node = node || this.selection.getCurrent(); while (node) @@ -6264,23 +7418,32 @@ node = node.parentNode; } return false; }, - getInlines: function(nodes) + getInlines: function(nodes, tags) { this.selection.get(); if (this.range && this.range.collapsed) { return false; } var inlines = []; - nodes = (typeof nodes == 'undefined') ? this.selection.getNodes() : nodes; + nodes = (typeof nodes == 'undefined' || nodes === false) ? this.selection.getNodes() : nodes; var inlineTags = this.opts.inlineTags; inlineTags.push('span'); + + if (typeof tags !== 'undefined') + { + for (var i = 0; i < tags.length; i++) + { + inlineTags.push(tags[i]); + } + } + $.each(nodes, $.proxy(function(i,node) { if ($.inArray(node.tagName.toLowerCase(), inlineTags) != -1) { inlines.push(node); @@ -6288,10 +7451,32 @@ }, this)); return (inlines.length === 0) ? false : inlines; }, + getInlinesTags: function(tags) + { + this.selection.get(); + + if (this.range && this.range.collapsed) + { + return false; + } + + var inlines = []; + var nodes = this.selection.getNodes(); + $.each(nodes, $.proxy(function(i,node) + { + if ($.inArray(node.tagName.toLowerCase(), tags) != -1) + { + inlines.push(node); + } + + }, this)); + + return (inlines.length === 0) ? false : inlines; + }, getBlocks: function(nodes) { this.selection.get(); if (this.range && this.range.collapsed) @@ -6322,18 +7507,37 @@ this.selection.get(); var startNode = this.selection.getNodesMarker(1); var endNode = this.selection.getNodesMarker(2); - this.selection.setNodesMarker(this.range, startNode, true); - if (this.range.collapsed === false) { - this.selection.setNodesMarker(this.range, endNode, false); + if (window.getSelection) { + var sel = window.getSelection(); + if (sel.rangeCount > 0) { + + var range = sel.getRangeAt(0); + var startPointNode = range.startContainer, startOffset = range.startOffset; + + var boundaryRange = range.cloneRange(); + boundaryRange.collapse(false); + boundaryRange.insertNode(endNode); + boundaryRange.setStart(startPointNode, startOffset); + boundaryRange.collapse(true); + boundaryRange.insertNode(startNode); + + // Reselect the original text + range.setStartAfter(startNode); + range.setEndBefore(endNode); + sel.removeAllRanges(); + sel.addRange(range); + } + } } else { + this.selection.setNodesMarker(this.range, startNode, true); endNode = startNode; } var nodes = []; var counter = 0; @@ -6387,11 +7591,11 @@ { return $('<span id="nodes-marker-' + num + '" class="redactor-nodes-marker" data-verified="redactor">' + this.opts.invisibleSpace + '</span>')[0]; }, setNodesMarker: function(range, node, type) { - range = range.cloneRange(); + var range = range.cloneRange(); try { range.collapse(type); range.insertNode(node); } @@ -6418,11 +7622,21 @@ return wrapper; }, selectElement: function(node) { - this.caret.set(node, 0, node, 1); + if (this.utils.browser('mozilla')) + { + node = node[0] || node; + + var range = document.createRange(); + range.selectNodeContents(node); + } + else + { + this.caret.set(node, 0, node, 1); + } }, selectAll: function() { this.selection.get(); this.range.selectNodeContents(this.$editor[0]); @@ -6442,11 +7656,10 @@ this.selection.get(); var node1 = this.selection.getMarker(1); this.selection.setMarker(this.range, node1, true); - if (this.range.collapsed === false) { var node2 = this.selection.getMarker(2); this.selection.setMarker(this.range, node2, false); } @@ -6468,21 +7681,28 @@ range = range.cloneRange(); try { range.collapse(type); range.insertNode(node); + } catch (e) { this.focus.setStart(); } + }, restore: function() { var node1 = this.$editor.find('span#selection-marker-1'); var node2 = this.$editor.find('span#selection-marker-2'); + if (this.utils.browser('mozilla')) + { + this.$editor.focus(); + } + if (node1.length !== 0 && node2.length !== 0) { this.caret.set(node1, 0, node2, 0); } else if (node1.length !== 0) @@ -6498,11 +7718,16 @@ this.savedSel = false; }, removeMarkers: function() { - this.$editor.find('span.redactor-selection-marker').remove(); + this.$editor.find('span.redactor-selection-marker').each(function(i,s) + { + var text = $(s).text().replace(/\u200B/g, ''); + if (text === '') $(s).remove(); + else $(s).replaceWith(function() { return $(this).contents(); }); + }); }, getText: function() { this.selection.get(); @@ -6524,10 +7749,41 @@ html = container.innerHTML; } return this.clean.onSync(html); + }, + replaceSelection: function(html) + { + this.selection.get(); + this.range.deleteContents(); + var div = document.createElement("div"); + div.innerHTML = html; + var frag = document.createDocumentFragment(), child; + while ((child = div.firstChild)) { + frag.appendChild(child); + } + + this.range.insertNode(frag); + }, + replaceWithHtml: function(html) + { + html = this.selection.getMarkerAsHtml(1) + html + this.selection.getMarkerAsHtml(2); + + this.selection.get(); + + if (window.getSelection && window.getSelection().getRangeAt) + { + this.selection.replaceSelection(html); + } + else if (document.selection && document.selection.createRange) + { + this.range.pasteHTML(html); + } + + this.selection.restore(); + this.code.sync(); } }; }, shortcuts: function() { @@ -6641,11 +7897,11 @@ if (!this.opts.tabifier) return code; // 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']; + var newLevel = ['p', 'blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'pre', 'select', 'td', 'th', 'tr', 'ul']; 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('|' ) + ')[ >]'); @@ -6806,10 +8062,11 @@ return tagout.replace(/\s*$/, '') + suffix + '>'; }, placeTag: function (tag, out) { var nl = tag.match(this.tabifier.newLevel); + if (tag.match(this.tabifier.lineBefore) || nl) { out = out.replace(/\s*$/, ''); out += '\n'; } @@ -6821,11 +8078,11 @@ out += tag; if (tag.match(this.tabifier.lineAfter) || tag.match(this.tabifier.newLevel)) { out = out.replace(/ *$/, ''); - out += '\n'; + //out += '\n'; } return out; } }; @@ -6833,10 +8090,16 @@ tidy: function() { return { setupAllowed: function() { + var index = $.inArray('span', this.opts.removeEmpty); + if (index !== -1) + { + this.opts.removeEmpty.splice(index, 1); + } + if (this.opts.allowedTags) this.opts.deniedTags = false; if (this.opts.allowedAttr) this.opts.removeAttr = false; if (this.opts.linebreaks) return; @@ -6922,28 +8185,17 @@ { rTags.push(this.tidy.settings.replaceTags[i][1]); replacement.push(this.tidy.settings.replaceTags[i][0]); } - this.tidy.$div.find(replacement.join(',')).each($.proxy(function(n,s) + $.each(replacement, $.proxy(function(key, value) { - var tag = rTags[n]; - $(s).replaceWith(function() + this.tidy.$div.find(value).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; + return $("<" + rTags[key] + " />", {html: $(this).html()}); }); - }, this)); - - return html; }, replaceStyles: function() { if (!this.tidy.settings.replaceStyles) return; @@ -6981,10 +8233,12 @@ if (this.tidy.settings.deniedTags) { this.tidy.$div.find(this.tidy.settings.deniedTags.join(',')).each(function(i, s) { + if ($(s).hasClass('redactor-script-tag') || $(s).hasClass('redactor-selection-marker')) return; + if (s.innerHTML === '') $(s).remove(); else $(s).contents().unwrap(); }); } }, @@ -7094,11 +8348,11 @@ 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(/\u200B/g, ''); text = text.replace(/&nbsp;/gi, ''); text = text.replace(/\s/g, ''); if (text === '' && $el.children().length === 0) { @@ -7248,16 +8502,34 @@ dropdown: { link: { title: this.lang.get('link_insert'), - func: 'link.show' + func: 'link.show', + observe: { + element: 'a', + in: { + title: this.lang.get('link_edit'), + }, + out: { + title: this.lang.get('link_insert') + } + } }, unlink: { title: this.lang.get('unlink'), - func: 'link.unlink' + func: 'link.unlink', + observe: { + element: 'a', + out: { + attr: { + 'class': 'redactor-dropdown-link-inactive', + 'aria-disabled': true + } + } + } } } }, alignment: { @@ -7310,17 +8582,17 @@ this.toolbar.setFixed(); // buttons response if (this.opts.activeButtons) { - this.$editor.on('mouseup.redactor keyup.redactor focus.redactor', $.proxy(this.observe.buttons, this)); + this.$editor.on('mouseup.redactor keyup.redactor focus.redactor', $.proxy(this.observe.toolbar, this)); } }, createContainer: function() { - return $('<ul>').addClass('redactor-toolbar').attr('id', 'redactor-toolbar-' + this.uuid); + return $('<ul>').addClass('redactor-toolbar').attr({'id': 'redactor-toolbar-' + this.uuid, 'role': 'toolbar'}); }, setFormattingTags: function() { $.each(this.opts.toolbar.formatting.dropdown, $.proxy(function (i, s) { @@ -7332,13 +8604,22 @@ { $.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; + if (btnName === 'file') + { + if (this.opts.fileUpload === false) return; + else if (!this.opts.fileUpload && this.opts.s3 === false) return; + } + if (btnName === 'image') + { + if (this.opts.imageUpload === false) return; + else if (!this.opts.imageUpload && this.opts.s3 === false) return; + } + var btnObject = this.opts.toolbar[btnName]; this.$toolbar.append($('<li>').append(this.button.build(btnName, btnObject))); }, this)); }, @@ -7359,11 +8640,11 @@ 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)); + $(this.opts.toolbarFixedTarget).on('scroll.redactor.' + this.uuid, $.proxy(this.toolbar.observeScroll, this)); }, setOverflow: function() { if (this.utils.isMobile() && this.opts.toolbarOverflow) @@ -7371,11 +8652,11 @@ this.$toolbar.addClass('redactor-toolbar-overflow'); } }, isButtonSourceNeeded: function() { - if (this.opts.buttonSource) return; + if (this.opts.source) return; var index = this.opts.buttons.indexOf('html'); if (index !== -1) { this.opts.buttons.splice(index, 1); @@ -7411,11 +8692,11 @@ if (this.opts.toolbarFixedTarget === document) { boxTop = this.$box.offset().top; } - if (scrollTop > boxTop) + if ((scrollTop + this.opts.toolbarFixedTopOffset) > boxTop) { this.toolbar.observeScrollEnable(scrollTop, boxTop); } else { @@ -7424,21 +8705,24 @@ }, observeScrollEnable: function(scrollTop, boxTop) { var top = this.opts.toolbarFixedTopOffset + scrollTop - boxTop; var left = 0; - var end = boxTop + this.$box.height() + 30; + var end = boxTop + this.$box.height() - 32; var width = this.$box.innerWidth(); this.$toolbar.addClass('toolbar-fixed-box'); this.$toolbar.css({ position: 'absolute', width: width, top: top + 'px', left: left }); + if (scrollTop > end) + $('.redactor-dropdown-' + this.uuid + ':visible').hide(); + this.toolbar.setDropdownsFixed(); this.$toolbar.css('visibility', (scrollTop < end) ? 'visible' : 'hidden'); }, observeScrollDisable: function() { @@ -7450,11 +8734,10 @@ visibility: 'visible' }); this.toolbar.unsetDropdownsFixed(); this.$toolbar.removeClass('toolbar-fixed-box'); - }, setDropdownsFixed: function() { var top = this.$toolbar.innerHeight() + this.opts.toolbarFixedTopOffset; var position = 'fixed'; @@ -7462,19 +8745,19 @@ { top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top) + this.opts.toolbarFixedTopOffset; position = 'absolute'; } - $('.redactor-dropdown').each(function() + $('.redactor-dropdown-' + this.uuid).each(function() { $(this).css({ position: position, top: top + 'px' }); }); }, unsetDropdownsFixed: function() { var top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top); - $('.redactor-dropdown').each(function() + $('.redactor-dropdown-' + this.uuid).each(function() { $(this).css({ position: 'absolute', top: top + 'px' }); }); } }; @@ -7488,11 +8771,11 @@ this.upload.callback = callback; this.upload.url = url; this.upload.$el = $(id); this.upload.$droparea = $('<div id="redactor-droparea" />'); - this.upload.$placeholdler = $('<div id="redactor-droparea-placeholder" />').text('Drop file here or '); + this.upload.$placeholdler = $('<div id="redactor-droparea-placeholder" />').text(this.lang.get('upload_label')); this.upload.$input = $('<input type="file" name="file" />'); this.upload.$placeholdler.append(this.upload.$input); this.upload.$droparea.append(this.upload.$placeholdler); this.upload.$el.append(this.upload.$droparea); @@ -7549,10 +8832,11 @@ var name = (this.upload.type == 'image') ? this.opts.imageUploadParam : this.opts.fileUploadParam; formData.append(name, file); } this.progress.show(); + this.core.setCallback('uploadStart', e, formData); this.upload.sendData(formData, e); }, setConfig: function(file) { this.upload.getType(file); @@ -7599,10 +8883,11 @@ formData = this.upload.getHiddenFields(this.upload.fileFields, formData); } var xhr = new XMLHttpRequest(); xhr.open('POST', this.upload.url); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); // complete xhr.onreadystatechange = $.proxy(function() { if (xhr.readyState == 4) @@ -7697,14 +8982,12 @@ }, this)); }, s3executeOnSignedUrl: function(file, callback) { var xhr = new XMLHttpRequest(); + var mark = (this.opts.s3.search(/\?/) !== '-1') ? '?' : '&'; - var mark = '?'; - if (this.opts.s3.search(/\?/) != '-1') mark = '&'; - xhr.open('GET', this.opts.s3 + mark + 'name=' + file.name + '&type=' + file.type, true); // Hack to pass bytes through unprocessed. if (xhr.overrideMimeType) xhr.overrideMimeType('text/plain; charset=x-user-defined'); @@ -7789,25 +9072,13 @@ { //setProgress(0, 'Upload error: ' + xhr.status); } }, this); - xhr.onerror = function() - { - //setProgress(0, 'XHR error.'); - }; + xhr.onerror = function() {}; - xhr.upload.onprogress = function(e) - { - /* - if (e.lengthComputable) - { - var percentLoaded = Math.round((e.loaded / e.total) * 100); - setProgress(percentLoaded, percentLoaded == 100 ? 'Finalizing.' : 'Uploading.'); - } - */ - }; + xhr.upload.onprogress = function(e) {}; xhr.setRequestHeader('Content-Type', file.type); xhr.setRequestHeader('x-amz-acl', 'public-read'); xhr.send(file); @@ -7836,10 +9107,11 @@ 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'); + html = html.replace(/<source(.*?[^>])>$/i, 'source'); // remove empty tags if (removeEmptyTags !== false) { html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, ''); @@ -7900,27 +9172,26 @@ return false; }, removeEmpty: function(i, s) { - var $s = $(s); + var $s = $($.parseHTML(s)); $s.find('.redactor-invisible-space').removeAttr('style').removeAttr('class'); - if ($s.find('hr, br, img, iframe').length !== 0) return; + if ($s.find('hr, br, img, iframe, source').length !== 0) return; var text = $.trim($s.text()); + if (this.utils.isEmpty(text, false)) { $s.remove(); } }, // save and restore scroll saveScroll: function() { - if (this.utils.isSelectAll()) return; - this.saveEditorScroll = this.$editor.scrollTop(); this.saveBodyScroll = $(window).scrollTop(); if (this.opts.scrollTarget) this.saveTargetScroll = $(this.opts.scrollTarget).scrollTop(); }, @@ -7961,10 +9232,12 @@ { if (removeInlineTags === true) self.utils.removeInlineTags(this); return $(this).contents(); }); + + return $(node); }, replaceToTag: function(node, tag, removeInlineTags) { var replacement; var self = this; @@ -7993,17 +9266,35 @@ var offset = this.caret.getOffsetOfElement(block); return (offset === 0) ? true : false; }, - isEndOfElement: function() + isEndOfElement: function(element) { - var block = this.selection.getBlock(); - if (!block) return false; + if (typeof element == 'undefined') + { + var element = this.selection.getBlock(); + if (!element) return false; + } + var offset = this.caret.getOffsetOfElement(element); + var text = $.trim($(element).text()).replace(/\n\r\n/g, ''); + + return (offset == text.length) ? true : false; + }, + isStartOfEditor: function() + { + var offset = this.caret.getOffsetOfElement(this.$editor[0]); + + return (offset === 0) ? true : false; + }, + isEndOfEditor: function() + { + var block = this.$editor[0]; + var offset = this.caret.getOffsetOfElement(block); - var text = $.trim($(block).text()).replace(/\n\r\n/g, ''); + var text = $.trim($(block).html().replace(/(<([^>]+)>)/gi,'')); return (offset == text.length) ? true : false; }, // test blocks @@ -8021,12 +9312,12 @@ }, // tag detection isTag: function(current, tag) { - var element = $(current).closest(tag); - if (element.size() == 1) + var element = $(current).closest(tag, this.$editor[0]); + if (element.length == 1) { return element[0]; } return false; @@ -8120,102 +9411,66 @@ /(msie) ([\w.]+)/.exec( ua ) || ua.indexOf("trident") >= 0 && /(rv)(?::| )([\w.]+)/.exec( ua ) || ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || []; + if (browser == 'safari') return (typeof match[3] != 'undefined') ? match[3] == 'safari' : false; if (browser == 'version') return match[2]; - if (browser == 'webkit') return (match[1] == 'chrome' || match[1] == 'webkit'); + if (browser == 'webkit') return (match[1] == 'chrome' || match[1] == 'opr' || match[1] == 'webkit'); if (match[1] == 'rv') return browser == 'msie'; if (match[1] == 'opr') return browser == 'webkit'; return browser == match[1]; - } - }; - } - }; - - // constructor - 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 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 && n.parentNode !== 'PRE') - { - var html = n.nodeValue; - - // youtube & vimeo - if (convertVideoLinks && html) + }, + strpos: function(haystack, needle, offset) { - var iframeStart = '<iframe width="500" height="281" src="', - iframeEnd = '" frameborder="0" allowfullscreen></iframe>'; + var i = haystack.indexOf(needle, offset); + return i >= 0 ? i : false; + }, + disableBodyScroll: function() + { - if (html.match(reUrlYoutube)) + var $body = $('html'); + var windowWidth = window.innerWidth; + if (!windowWidth) { - html = html.replace(reUrlYoutube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd); - $(n).after(html).remove(); + var documentElementRect = document.documentElement.getBoundingClientRect(); + windowWidth = documentElementRect.right - Math.abs(documentElementRect.left); } - else if (html.match(reUrlVimeo)) - { - html = html.replace(reUrlVimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd); - $(n).after(html).remove(); - } - } - // image - if (convertImageLinks && html && html.match(urlImage)) - { - html = html.replace(urlImage, '<img src="$1" />'); + var isOverflowing = document.body.clientWidth < windowWidth; + var scrollbarWidth = this.utils.measureScrollbar(); - $(n).after(html).remove(); - return; - } + $body.css('overflow', 'hidden'); + if (isOverflowing) $body.css('padding-right', scrollbarWidth); - // link - if (html.search(/\$/g) != -1) html = html.replace(/\$/g, '&#36;'); - var matches = html.match(regex); - if (convertUrlLinks && html && matches) + }, + measureScrollbar: function() { + var $body = $('body'); + var scrollDiv = document.createElement('div'); + scrollDiv.className = 'redactor-scrollbar-measure'; - var len = matches.length; - for (var z = 0; z < len; z++) - { - // remove dot in the end - if (matches[z].match(/\.$/) !== null) matches[z] = matches[z].replace(/\.$/, ''); - - var href = matches[z]; - var text = href; - - var space = ''; - if (href.match(/\s$/) !== null) space = ' '; - - var addProtocol = protocol + '://'; - if (href.match(rProtocol) !== null) addProtocol = ''; - - if (text.length > linkSize) text = text.substring(0, linkSize) + '...'; - text = text.replace(/&#36;/g, '$').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); - - html = html.replace(href, '<a href=\"' + addProtocol + $.trim(href) + '\">' + $.trim(text) + '</a>' + space); - } - - $(n).after(html).remove(); + $body.append(scrollDiv); + var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; + $body[0].removeChild(scrollDiv); + return scrollbarWidth; + }, + enableBodyScroll: function() + { + $('html').css({ 'overflow': '', 'padding-right': '' }); + $('body').remove('redactor-scrollbar-measure'); } - } - else if (n.nodeType === 1 && !/^(pre|a|button|textarea)$/i.test(n.tagName)) - { - $.Redactor.fn.formatLinkify.call(n, protocol, convertLinks, convertUrlLinks, convertImageLinks, convertVideoLinks, linkSize); - } + }; } }; + + $(window).on('load.tools.redactor', function() + { + $('[data-tools="redactor"]').redactor(); + }); + + // constructor + Redactor.prototype.init.prototype = Redactor.prototype; })(jQuery); \ No newline at end of file