app/assets/javascripts/redactor.js in scrivito_editors-0.30.1 vs app/assets/javascripts/redactor.js in scrivito_editors-0.40.0.rc1

- old
+ new

@@ -1,38 +1,36 @@ /* - Redactor v9.2.6 - Updated: Jul 19, 2014 + Redactor v10.0.6 + Updated: January 7, 2015 http://imperavi.com/redactor/ - Copyright (c) 2009-2014, Imperavi LLC. + Copyright (c) 2009-2015, Imperavi LLC. License: http://imperavi.com/redactor/license/ Usage: $('#content').redactor(); */ + (function($) { - var uuid = 0; + 'use strict'; - "use strict"; - - var Range = function(range) + if (!Function.prototype.bind) { - this[0] = range.startOffset; - this[1] = range.endOffset; + Function.prototype.bind = function(scope) + { + var fn = this; + return function() + { + return fn.apply(scope); + }; + }; + } - this.range = range; + var uuid = 0; - return this; - }; - - Range.prototype.equals = function() - { - return this[0] === this[1]; - }; - - var reUrlYoutube = /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig; + 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) { @@ -42,23 +40,45 @@ if (typeof options === 'string') { this.each(function() { var instance = $.data(this, 'redactor'); - if (typeof instance !== 'undefined' && $.isFunction(instance[options])) + var func; + + if (options.search(/\./) != '-1') { - var methodVal = instance[options].apply(instance, args); - if (methodVal !== undefined && methodVal !== instance) val.push(methodVal); + func = options.split('.'); + if (typeof instance[func[0]] != 'undefined') + { + func = instance[func[0]][func[1]]; + } } - else return $.error('No such method "' + options + '" for Redactor'); + else + { + func = instance[options]; + } + + if (typeof instance !== 'undefined' && $.isFunction(func)) + { + var methodVal = func.apply(instance, args); + if (methodVal !== undefined && methodVal !== instance) + { + val.push(methodVal); + } + } + else + { + $.error('No such method "' + options + '" for Redactor'); + } }); } else { this.each(function() { - if (!$.data(this, 'redactor')) $.data(this, 'redactor', Redactor(this, options)); + $.data(this, 'redactor', {}); + $.data(this, 'redactor', Redactor(this, options)); }); } if (val.length === 0) return this; else if (val.length === 1) return val[0]; @@ -70,8165 +90,8068 @@ function Redactor(el, options) { return new Redactor.prototype.init(el, options); } + // Functionality $.Redactor = Redactor; - $.Redactor.VERSION = '9.2.6'; + $.Redactor.VERSION = '10.0.6'; + $.Redactor.modules = ['alignment', 'autosave', 'block', 'buffer', 'build', 'button', + 'caret', 'clean', 'code', 'core', 'dropdown', 'file', 'focus', + 'image', 'indent', 'inline', 'insert', 'keydown', 'keyup', + 'lang', 'line', 'link', 'list', 'modal', 'observe', 'paragraphize', + 'paste', 'placeholder', 'progress', 'selection', 'shortcuts', + 'tabifier', 'tidy', 'toolbar', 'upload', 'utils']; + $.Redactor.opts = { - // settings - rangy: false, + // settings + lang: 'en', + direction: 'ltr', // ltr or rtl - iframe: false, - fullpage: false, - css: false, // url + plugins: false, // array - lang: 'en', - direction: 'ltr', // ltr or rtl + focus: false, + focusEnd: false, - placeholder: false, + placeholder: false, - typewriter: false, - wym: false, - mobile: true, - cleanup: true, - tidyHtml: true, - pastePlainText: false, - removeEmptyTags: true, - cleanSpaces: true, - cleanFontTag: true, - templateVars: false, - xhtml: false, + visual: true, + tabindex: false, - visual: true, - focus: false, - tabindex: false, - autoresize: true, - minHeight: false, - maxHeight: false, - shortcuts: { - 'ctrl+m, meta+m': "this.execCommand('removeFormat', false)", - 'ctrl+b, meta+b': "this.execCommand('bold', false)", - 'ctrl+i, meta+i': "this.execCommand('italic', false)", - 'ctrl+h, meta+h': "this.execCommand('superscript', false)", - 'ctrl+l, meta+l': "this.execCommand('subscript', false)", - 'ctrl+k, meta+k': "this.linkShow()", - 'ctrl+shift+7': "this.execCommand('insertorderedlist', false)", - 'ctrl+shift+8': "this.execCommand('insertunorderedlist', false)" - }, - shortcutsAdd: false, + minHeight: false, + maxHeight: false, - autosave: false, // false or url - autosaveInterval: 60, // seconds + linebreaks: false, + replaceDivs: true, + paragraphize: true, + cleanStyleOnEnter: false, + enterKey: true, - plugins: false, // array + cleanOnPaste: true, + cleanSpaces: true, + pastePlainText: false, - //linkAnchor: true, - //linkEmail: true, - linkProtocol: 'http://', - linkNofollow: false, - linkSize: 50, - predefinedLinks: false, // json url (ex. /some-url.json ) or false + autosave: false, // false or url + autosaveName: false, + autosaveInterval: 60, // seconds + autosaveOnChange: false, - imageFloatMargin: '10px', - imageGetJson: false, // json url (ex. /some-images.json ) or false + linkTooltip: true, + linkProtocol: 'http', + linkNofollow: false, + linkSize: 50, - dragUpload: true, // false - imageTabLink: true, - imageUpload: false, // url - imageUploadParam: 'file', // input name - imageResizable: true, + imageEditable: true, + imageLink: true, + imagePosition: true, + imageFloatMargin: '10px', + imageResizable: true, - fileUpload: false, // url - fileUploadParam: 'file', // input name - clipboardUpload: true, // or false - clipboardUploadUrl: false, // url + imageUpload: false, + imageUploadParam: 'file', - dnbImageTypes: ['image/png', 'image/jpeg', 'image/gif'], // or false + uploadImageField: false, - s3: false, - uploadFields: false, + dragImageUpload: true, - observeImages: true, - observeLinks: true, + fileUpload: false, + fileUploadParam: 'file', - modalOverlay: true, + dragFileUpload: true, - tabSpaces: false, // true or number of spaces - tabFocus: true, + s3: false, - air: false, - airButtons: ['formatting', 'bold', 'italic', 'deleted', 'unorderedlist', 'orderedlist', 'outdent', 'indent'], + convertLinks: true, + convertUrlLinks: true, + convertImageLinks: true, + convertVideoLinks: true, - toolbar: true, - toolbarFixed: false, - toolbarFixedTarget: document, - toolbarFixedTopOffset: 0, // pixels - toolbarFixedBox: false, - toolbarExternal: false, // ID selector - toolbarOverflow: false, - buttonSource: true, + preSpaces: 4, // or false + tabAsSpaces: false, // true or number of spaces + tabKey: true, - buttons: ['html', 'formatting', 'bold', 'italic', 'deleted', 'unorderedlist', 'orderedlist', - 'outdent', 'indent', 'image', 'video', 'file', 'table', 'link', 'alignment', '|', - 'horizontalrule'], // 'underline', 'alignleft', 'aligncenter', 'alignright', 'justify' - buttonsHideOnMobile: [], + scrollTarget: false, - activeButtons: ['deleted', 'italic', 'bold', 'underline', 'unorderedlist', 'orderedlist', - 'alignleft', 'aligncenter', 'alignright', 'justify', 'table'], - activeButtonsStates: { - b: 'bold', - strong: 'bold', - i: 'italic', - em: 'italic', - del: 'deleted', - strike: 'deleted', - ul: 'unorderedlist', - ol: 'orderedlist', - u: 'underline', - tr: 'table', - td: 'table', - table: 'table' - }, + toolbar: true, + toolbarFixed: true, + toolbarFixedTarget: document, + toolbarFixedTopOffset: 0, // pixels + toolbarExternal: false, // ID selector + toolbarOverflow: false, - formattingTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + buttonSource: false, + buttons: ['html', 'formatting', 'bold', 'italic', 'deleted', 'unorderedlist', 'orderedlist', + 'outdent', 'indent', 'image', 'file', 'link', 'alignment', 'horizontalrule'], // + 'underline' - linebreaks: false, - paragraphy: true, - convertDivs: true, - convertLinks: true, - convertImageLinks: false, - convertVideoLinks: false, - formattingPre: false, - phpTags: false, + buttonsHide: [], + buttonsHideOnMobile: [], - allowedTags: false, - deniedTags: ['html', 'head', 'link', 'body', 'meta', 'script', 'style', 'applet'], + formatting: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + formattingAdd: false, - boldTag: 'strong', - italicTag: 'em', + tabifier: true, - // private - indentValue: 20, - buffer: [], - rebuffer: [], - textareamode: false, - emptyHtml: '<p>&#x200b;</p>', - invisibleSpace: '&#x200b;', - rBlockTest: /^(P|H[1-6]|LI|ADDRESS|SECTION|HEADER|FOOTER|ASIDE|ARTICLE)$/i, - alignmentTags: ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DD', 'DL', 'DT', 'DIV', 'TD', - 'BLOCKQUOTE', 'OUTPUT', 'FIGCAPTION', 'ADDRESS', 'SECTION', - 'HEADER', 'FOOTER', 'ASIDE', 'ARTICLE'], - ownLine: ['area', 'body', 'head', 'hr', 'i?frame', 'link', 'meta', 'noscript', 'style', 'script', 'table', 'tbody', 'thead', 'tfoot'], - contOwnLine: ['li', 'dt', 'dt', 'h[1-6]', 'option', 'script'], - newLevel: ['blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'p', 'pre', 'select', 'td', 'th', 'tr', 'ul'], - blockLevelElements: ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DD', 'DL', 'DT', 'DIV', 'LI', - 'BLOCKQUOTE', 'OUTPUT', 'FIGCAPTION', 'PRE', 'ADDRESS', 'SECTION', - 'HEADER', 'FOOTER', 'ASIDE', 'ARTICLE', 'TD'], + deniedTags: ['html', 'head', 'link', 'body', 'meta', 'script', 'style', 'applet'], + allowedTags: false, // or array + removeComments: false, + replaceTags: [ + ['strike', 'del'] + ], + replaceStyles: [ + ['font-weight:\\s?bold', "strong"], + ['font-style:\\s?italic', "em"], + ['text-decoration:\\s?underline', "u"], + ['text-decoration:\\s?line-through', 'del'] + ], + removeDataAttr: false, - // lang - langs: { - en: { - html: 'HTML', - video: 'Insert Video', - image: 'Insert Image', - table: 'Table', - link: 'Link', - link_insert: 'Insert link', - link_edit: 'Edit link', - unlink: 'Unlink', - formatting: 'Formatting', - paragraph: 'Normal text', - quote: 'Quote', - code: 'Code', - header1: 'Header 1', - header2: 'Header 2', - header3: 'Header 3', - header4: 'Header 4', - header5: 'Header 5', - bold: 'Bold', - italic: 'Italic', - fontcolor: 'Font Color', - backcolor: 'Back Color', - unorderedlist: 'Unordered List', - orderedlist: 'Ordered List', - outdent: 'Outdent', - indent: 'Indent', - cancel: 'Cancel', - insert: 'Insert', - save: 'Save', - _delete: 'Delete', - insert_table: 'Insert Table', - insert_row_above: 'Add Row Above', - insert_row_below: 'Add Row Below', - insert_column_left: 'Add Column Left', - insert_column_right: 'Add Column Right', - delete_column: 'Delete Column', - delete_row: 'Delete Row', - delete_table: 'Delete Table', - rows: 'Rows', - columns: 'Columns', - add_head: 'Add Head', - delete_head: 'Delete Head', - title: 'Title', - image_position: 'Position', - none: 'None', - left: 'Left', - right: 'Right', - center: 'Center', - image_web_link: 'Image Web Link', - text: 'Text', - mailto: 'Email', - web: 'URL', - video_html_code: 'Video Embed Code', - file: 'Insert File', - upload: 'Upload', - download: 'Download', - choose: 'Choose', - or_choose: 'Or choose', - drop_file_here: 'Drop file here', - align_left: 'Align text to the left', - align_center: 'Center text', - align_right: 'Align text to the right', - align_justify: 'Justify text', - horizontalrule: 'Insert Horizontal Rule', - deleted: 'Deleted', - anchor: 'Anchor', - link_new_tab: 'Open link in new tab', - underline: 'Underline', - alignment: 'Alignment', - filename: 'Name (optional)', - edit: 'Edit' - } + removeAttr: false, // or multi array + allowedAttr: false, // or multi array + + removeWithoutAttr: ['span'], // or false + removeEmpty: ['p'], // or false; + + activeButtons: ['deleted', 'italic', 'bold', 'underline', 'unorderedlist', 'orderedlist', + 'alignleft', 'aligncenter', 'alignright', 'justify'], + activeButtonsStates: { + b: 'bold', + strong: 'bold', + i: 'italic', + em: 'italic', + del: 'deleted', + strike: 'deleted', + ul: 'unorderedlist', + ol: 'orderedlist', + u: 'underline' + }, + + shortcuts: { + 'ctrl+shift+m, meta+shift+m': { func: 'inline.removeFormat' }, + 'ctrl+b, meta+b': { func: 'inline.format', params: ['bold'] }, + 'ctrl+i, meta+i': { func: 'inline.format', params: ['italic'] }, + 'ctrl+h, meta+h': { func: 'inline.format', params: ['superscript'] }, + 'ctrl+l, meta+l': { func: 'inline.format', params: ['subscript'] }, + 'ctrl+k, meta+k': { func: 'link.show' }, + 'ctrl+shift+7': { func: 'list.toggle', params: ['orderedlist'] }, + 'ctrl+shift+8': { func: 'list.toggle', params: ['unorderedlist'] } + }, + shortcutsAdd: false, + + // private + buffer: [], + rebuffer: [], + emptyHtml: '<p>&#x200b;</p>', + invisibleSpace: '&#x200b;', + imageTypes: ['image/png', 'image/jpeg', 'image/gif'], + 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'], + + + // lang + langs: { + en: { + html: 'HTML', + video: 'Insert Video', + image: 'Insert Image', + table: 'Table', + link: 'Link', + link_insert: 'Insert link', + link_edit: 'Edit link', + unlink: 'Unlink', + formatting: 'Formatting', + paragraph: 'Normal text', + quote: 'Quote', + code: 'Code', + header1: 'Header 1', + header2: 'Header 2', + header3: 'Header 3', + header4: 'Header 4', + header5: 'Header 5', + bold: 'Bold', + italic: 'Italic', + fontcolor: 'Font Color', + backcolor: 'Back Color', + unorderedlist: 'Unordered List', + orderedlist: 'Ordered List', + outdent: 'Outdent', + indent: 'Indent', + cancel: 'Cancel', + insert: 'Insert', + save: 'Save', + _delete: 'Delete', + insert_table: 'Insert Table', + insert_row_above: 'Add Row Above', + insert_row_below: 'Add Row Below', + insert_column_left: 'Add Column Left', + insert_column_right: 'Add Column Right', + delete_column: 'Delete Column', + delete_row: 'Delete Row', + delete_table: 'Delete Table', + rows: 'Rows', + columns: 'Columns', + add_head: 'Add Head', + delete_head: 'Delete Head', + title: 'Title', + image_position: 'Position', + none: 'None', + left: 'Left', + right: 'Right', + center: 'Center', + image_web_link: 'Image Web Link', + text: 'Text', + mailto: 'Email', + web: 'URL', + video_html_code: 'Video Embed Code or Youtube/Vimeo Link', + file: 'Insert File', + upload: 'Upload', + download: 'Download', + choose: 'Choose', + or_choose: 'Or choose', + drop_file_here: 'Drop file here', + align_left: 'Align text to the left', + align_center: 'Center text', + align_right: 'Align text to the right', + align_justify: 'Justify text', + horizontalrule: 'Insert Horizontal Rule', + deleted: 'Deleted', + anchor: 'Anchor', + link_new_tab: 'Open link in new tab', + underline: 'Underline', + alignment: 'Alignment', + filename: 'Name (optional)', + edit: 'Edit' } + } }; // Functionality Redactor.fn = $.Redactor.prototype = { keyCode: { BACKSPACE: 8, DELETE: 46, DOWN: 40, ENTER: 13, + SPACE: 32, ESC: 27, TAB: 9, CTRL: 17, META: 91, + SHIFT: 16, + ALT: 18, + RIGHT: 39, LEFT: 37, LEFT_WIN: 91 }, // Initialization init: function(el, options) { - this.rtePaste = false; - this.$element = this.$source = $(el); + this.$element = $(el); this.uuid = uuid++; - // clonning options - var opts = $.extend(true, {}, $.Redactor.opts); + // if paste event detected = true + this.rtePaste = false; + this.$pasteBox = false; - // current settings - this.opts = $.extend( - {}, - opts, - this.$element.data(), - options - ); + this.loadOptions(options); + this.loadModules(); - this.start = true; - this.dropdowns = []; + // formatting storage + this.formatting = {}; - // get sizes - this.sourceHeight = this.$source.css('height'); - this.sourceWidth = this.$source.css('width'); + // block level tags + $.merge(this.opts.blockLevelElements, this.opts.alignmentTags); + this.reIsBlock = new RegExp('^(' + this.opts.blockLevelElements.join('|' ) + ')$', 'i'); - // dependency of the editor modes - if (this.opts.fullpage) this.opts.iframe = true; - if (this.opts.linebreaks) this.opts.paragraphy = false; - if (this.opts.paragraphy) this.opts.linebreaks = false; - if (this.opts.toolbarFixedBox) this.opts.toolbarFixed = true; + // setup allowed and denied tags + this.tidy.setupAllowed(); - // the alias for iframe mode - this.document = document; - this.window = window; + // load lang + this.lang.load(); - // selection saved - this.savedSel = false; + // extend shortcuts + $.extend(this.opts.shortcuts, this.opts.shortcutsAdd); - // clean setup - this.cleanlineBefore = new RegExp('^<(/?' + this.opts.ownLine.join('|/?' ) + '|' + this.opts.contOwnLine.join('|') + ')[ >]'); - this.cleanlineAfter = new RegExp('^<(br|/?' + this.opts.ownLine.join('|/?' ) + '|/' + this.opts.contOwnLine.join('|/') + ')[ >]'); - this.cleannewLevel = new RegExp('^</?(' + this.opts.newLevel.join('|' ) + ')[ >]'); + // start callback + this.core.setCallback('start'); - // block level - this.rTestBlock = new RegExp('^(' + this.opts.blockLevelElements.join('|' ) + ')$', 'i'); + // build + this.start = true; + this.build.run(); + }, - // setup formatting permissions - if (this.opts.linebreaks === false) + loadOptions: function(options) + { + this.opts = $.extend( + {}, + $.extend(true, {}, $.Redactor.opts), + this.$element.data(), + options + ); + }, + getModuleMethods: function(object) + { + return Object.getOwnPropertyNames(object).filter(function(property) { - if (this.opts.allowedTags !== false) - { - var arrSearch = ['strong', 'em', 'del']; - var arrAdd = ['b', 'i', 'strike']; - - if ($.inArray('p', this.opts.allowedTags) === '-1') this.opts.allowedTags.push('p'); - - for (i in arrSearch) - { - if ($.inArray(arrSearch[i], this.opts.allowedTags) != '-1') this.opts.allowedTags.push(arrAdd[i]); - } - } - - if (this.opts.deniedTags !== false) - { - var pos = $.inArray('p', this.opts.deniedTags); - if (pos !== '-1') this.opts.deniedTags.splice(pos, pos); - } - } - - // ie & opera - if (this.browser('msie') || this.browser('opera')) + return typeof object[property] == 'function'; + }); + }, + loadModules: function() + { + var len = $.Redactor.modules.length; + for (var i = 0; i < len; i++) { - this.opts.buttons = this.removeFromArrayByValue(this.opts.buttons, 'horizontalrule'); + this.bindModuleMethods($.Redactor.modules[i]); } + }, + bindModuleMethods: function(module) + { + if (typeof this[module] == 'undefined') return; - // load lang - this.opts.curLang = this.opts.langs[this.opts.lang]; + // init module + this[module] = this[module](); - // extend shortcuts - $.extend(this.opts.shortcuts, this.opts.shortcutsAdd); + var methods = this.getModuleMethods(this[module]); + var len = methods.length; - // init placeholder - this.placeholderInit(); - - // Build - this.buildStart(); - + // bind methods + for (var z = 0; z < len; z++) + { + this[module][methods[z]] = this[module][methods[z]].bind(this); + } }, - toolbarInit: function(lang) + + alignment: function() { return { - html: + left: function() { - title: lang.html, - func: 'toggle' + this.alignment.set(''); }, - formatting: + right: function() { - title: lang.formatting, - func: 'show', - dropdown: - { - p: - { - title: lang.paragraph, - func: 'formatBlocks' - }, - blockquote: - { - title: lang.quote, - func: 'formatQuote', - className: 'redactor_format_blockquote' - }, - pre: - { - title: lang.code, - func: 'formatBlocks', - className: 'redactor_format_pre' - }, - h1: - { - title: lang.header1, - func: 'formatBlocks', - className: 'redactor_format_h1' - }, - h2: - { - title: lang.header2, - func: 'formatBlocks', - className: 'redactor_format_h2' - }, - h3: - { - title: lang.header3, - func: 'formatBlocks', - className: 'redactor_format_h3' - }, - h4: - { - title: lang.header4, - func: 'formatBlocks', - className: 'redactor_format_h4' - }, - h5: - { - title: lang.header5, - func: 'formatBlocks', - className: 'redactor_format_h5' - } - } + this.alignment.set('right'); }, - bold: + center: function() { - title: lang.bold, - exec: 'bold' + this.alignment.set('center'); }, - italic: + justify: function() { - title: lang.italic, - exec: 'italic' + this.alignment.set('justify'); }, - deleted: + set: function(type) { - title: lang.deleted, - exec: 'strikethrough' + if (!this.utils.browser('msie')) this.$editor.focus(); + + this.buffer.set(); + this.selection.save(); + + this.alignment.blocks = this.selection.getBlocks(); + if (this.opts.linebreaks && this.alignment.blocks[0] === false) + { + this.alignment.setText(type); + } + else + { + this.alignment.setBlocks(type); + } + + this.selection.restore(); + this.code.sync(); }, - underline: + setText: function(type) { - title: lang.underline, - exec: 'underline' + var wrapper = this.selection.wrap('div'); + $(wrapper).attr('data-tagblock', 'redactor'); + $(wrapper).css('text-align', type); }, - unorderedlist: + setBlocks: function(type) { - title: '&bull; ' + lang.unorderedlist, - exec: 'insertunorderedlist' - }, - orderedlist: - { - title: '1. ' + lang.orderedlist, - exec: 'insertorderedlist' - }, - outdent: - { - title: '< ' + lang.outdent, - func: 'indentingOutdent' - }, - indent: - { - title: '> ' + lang.indent, - func: 'indentingIndent' - }, - image: - { - title: lang.image, - func: 'imageShow' - }, - video: - { - title: lang.video, - func: 'videoShow' - }, - file: - { - title: lang.file, - func: 'fileShow' - }, - table: - { - title: lang.table, - func: 'show', - dropdown: + $.each(this.alignment.blocks, $.proxy(function(i, el) { - insert_table: + var $el = this.utils.getAlignmentElement(el); + + if (!$el) return; + + if (type === '' && typeof($el.data('tagblock')) !== 'undefined') { - title: lang.insert_table, - func: 'tableShow' - }, - separator_drop1: - { - name: 'separator' - }, - insert_row_above: - { - title: lang.insert_row_above, - func: 'tableAddRowAbove' - }, - insert_row_below: - { - title: lang.insert_row_below, - func: 'tableAddRowBelow' - }, - insert_column_left: - { - title: lang.insert_column_left, - func: 'tableAddColumnLeft' - }, - insert_column_right: - { - title: lang.insert_column_right, - func: 'tableAddColumnRight' - }, - separator_drop2: - { - name: 'separator' - }, - add_head: - { - title: lang.add_head, - func: 'tableAddHead' - }, - delete_head: - { - title: lang.delete_head, - func: 'tableDeleteHead' - }, - separator_drop3: - { - name: 'separator' - }, - delete_column: - { - title: lang.delete_column, - func: 'tableDeleteColumn' - }, - delete_row: - { - title: lang.delete_row, - func: 'tableDeleteRow' - }, - delete_table: - { - title: lang.delete_table, - func: 'tableDeleteTable' + $el.replaceWith($el.html()); } - } - }, - link: { - title: lang.link, - func: 'show', - dropdown: - { - link: + else { - title: lang.link_insert, - func: 'linkShow' - }, - unlink: - { - title: lang.unlink, - exec: 'unlink' + $el.css('text-align', type); + this.utils.removeEmptyAttr($el, 'style'); } - } - }, - alignment: + + + }, this)); + } + }; + }, + autosave: function() + { + return { + enable: function() { - title: lang.alignment, - func: 'show', - dropdown: + 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) { - alignleft: - { - title: lang.align_left, - func: 'alignmentLeft' - }, - aligncenter: - { - title: lang.align_center, - func: 'alignmentCenter' - }, - alignright: - { - title: lang.align_right, - func: 'alignmentRight' - }, - justify: - { - title: lang.align_justify, - func: 'alignmentJustify' - } + this.autosaveInterval = setInterval($.proxy(this.autosave.load, this), this.opts.autosaveInterval * 1000); } }, - alignleft: + onChange: function() { - title: lang.align_left, - func: 'alignmentLeft' + if (!this.opts.autosaveOnChange) return; + + this.autosave.load(); }, - aligncenter: + load: function() { - title: lang.align_center, - func: 'alignmentCenter' + var html = this.code.get(); + if (this.autosave.html === html) return; + if (this.utils.isEmpty(html)) return; + + $.ajax({ + url: this.opts.autosave, + type: 'post', + data: 'name=' + this.autosave.name + '&' + this.autosave.name + '=' + escape(encodeURIComponent(html)), + success: $.proxy(function(data) + { + this.autosave.success(data, html); + + }, this) + }); }, - alignright: + success: function(data, html) { - title: lang.align_right, - func: 'alignmentRight' + var json; + try + { + json = $.parseJSON(data); + } + catch(e) + { + //data has already been parsed + json = data; + } + + var callbackName = (typeof json.error == 'undefined') ? 'autosave' : 'autosaveError'; + + this.core.setCallback(callbackName, this.autosave.name, json); + this.autosave.html = html; }, - alignjustify: + disable: function() { - title: lang.align_justify, - func: 'alignmentJustify' - }, - horizontalrule: - { - exec: 'inserthorizontalrule', - title: lang.horizontalrule + clearInterval(this.autosaveInterval); } - - } + }; }, - - // CALLBACKS - callback: function(type, event, data) + block: function() { - var callback = this.opts[ type + 'Callback' ]; - if ($.isFunction(callback)) - { - if (event === false) return callback.call(this, data); - else return callback.call(this, event, data); - } - else return data; - }, + return { + formatting: function(name) + { + this.block.clearStyle = false; + var type, value; + if (typeof this.formatting[name].data != 'undefined') type = 'data'; + else if (typeof this.formatting[name].attr != 'undefined') type = 'attr'; + else if (typeof this.formatting[name].class != 'undefined') type = 'class'; - // DESTROY - destroy: function() - { - clearInterval(this.autosaveInterval); + if (typeof this.formatting[name].clear != 'undefined') + { + this.block.clearStyle = true; + } - $(window).off('.redactor'); - this.$source.off('redactor-textarea'); - this.$element.off('.redactor').removeData('redactor'); + if (type) value = this.formatting[name][type]; - var html = this.get(); + this.block.format(this.formatting[name].tag, type, value); - if (this.opts.textareamode) - { - this.$box.after(this.$source); - this.$box.remove(); - this.$source.val(html).show(); - } - else - { - var $elem = this.$editor; - if (this.opts.iframe) $elem = this.$element; + }, + format: function(tag, type, value) + { + if (tag == 'quote') tag = 'blockquote'; - this.$box.after($elem); - this.$box.remove(); + var formatTags = ['p', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + if ($.inArray(tag, formatTags) == -1) return; - $elem.removeClass('redactor_editor').removeClass('redactor_editor_wym').removeAttr('contenteditable').html(html).show(); - } + this.block.isRemoveInline = (tag == 'pre' || tag.search(/h[1-6]/i) != -1); - if (this.opts.toolbarExternal) - { - $(this.opts.toolbarExternal).html(''); - } + // focus + if (!this.utils.browser('msie')) this.$editor.focus(); - if (this.opts.air) - { - $('#redactor_air_' + this.uuid).remove(); - } - }, + this.block.blocks = this.selection.getBlocks(); - // API GET - getObject: function() - { - return $.extend({}, this); - }, - getEditor: function() - { - return this.$editor; - }, - getBox: function() - { - return this.$box; - }, - getIframe: function() - { - return (this.opts.iframe) ? this.$frame : false; - }, - getToolbar: function() - { - return (this.$toolbar) ? this.$toolbar : false; - }, + this.block.blocksSize = this.block.blocks.length; + this.block.type = type; + this.block.value = value; - // CODE GET & SET - get: function() - { - return this.$source.val(); - }, - getCodeIframe: function() - { - this.$editor.removeAttr('contenteditable').removeAttr('dir'); - var html = this.outerHtml(this.$frame.contents().children()); - this.$editor.attr({ 'contenteditable': true, 'dir': this.opts.direction }); + this.buffer.set(); + this.selection.save(); - return html; - }, - set: function(html, strip, placeholderRemove) - { - html = html.toString(); - html = html.replace(/\$/g, '&#36;'); + this.block.set(tag); - if (this.opts.fullpage) this.setCodeIframe(html); - else this.setEditor(html, strip); + this.selection.restore(); + this.code.sync(); - if (html == '') placeholderRemove = false; - if (placeholderRemove !== false) this.placeholderRemoveFromEditor(); - }, - setEditor: function(html, strip) - { + }, + set: function(tag) + { + this.selection.get(); + this.block.containerTag = this.range.commonAncestorContainer.tagName; - if (strip !== false) - { - html = this.cleanSavePreCode(html); + if (this.range.collapsed) + { + this.block.setCollapsed(tag); + } + else + { + this.block.setMultiple(tag); + } + }, + setCollapsed: function(tag) + { + var block = this.block.blocks[0]; + if (block === false) return; - html = this.cleanStripTags(html); - html = this.cleanConvertProtected(html); - html = this.cleanConvertInlineTags(html, true); + if (block.tagName == 'LI') + { + if (tag != 'blockquote') return; - if (this.opts.linebreaks === false) html = this.cleanConverters(html); - else html = html.replace(/<p(.*?)>([\w\W]*?)<\/p>/gi, '$2<br>'); - } + this.block.formatListToBlockquote(); + return; + } - // $ fix - html = html.replace(/&amp;#36;/g, '$'); + var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); + if (isContainerTable && !this.opts.linebreaks) + { - html = this.cleanEmpty(html); + document.execCommand('formatblock', false, '<' + tag + '>'); - this.$editor.html(html); + block = this.selection.getBlock(); + this.block.toggle($(block)); - // set no editable - this.setNonEditable(); - this.setSpansVerified(); + } + else if (block.tagName.toLowerCase() != tag) + { + if (this.opts.linebreaks && tag == 'p') + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else + { + var $formatted = this.utils.replaceToTag(block, tag); - this.sync(); - }, - setCodeIframe: function(html) - { - var doc = this.iframePage(); - this.$frame[0].src = "about:blank"; + this.block.toggle($formatted); - html = this.cleanConvertProtected(html); - html = this.cleanConvertInlineTags(html); - html = this.cleanRemoveSpaces(html); + 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(); - doc.open(); - doc.write(html); - doc.close(); + this.block.formatTableWrapping($formatted); + } + } + else if (tag == 'blockquote' && block.tagName.toLowerCase() == tag) + { + // blockquote off + if (this.opts.linebreaks) + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else + { + var $el = this.utils.replaceToTag(block, 'p'); + this.block.toggle($el); + } + } + else if (block.tagName.toLowerCase() == tag) + { + this.block.toggle($(block)); + } - // redefine editor for fullpage mode - if (this.opts.fullpage) - { - this.$editor = this.$frame.contents().find('body').attr({ 'contenteditable': true, 'dir': this.opts.direction }); - } + }, + setMultiple: function(tag) + { + var block = this.block.blocks[0]; + var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH'); - // set no editable - this.setNonEditable(); - this.setSpansVerified(); - this.sync(); + if (block !== false && this.block.blocksSize === 1) + { + if (block.tagName.toLowerCase() == tag && tag == 'blockquote') + { + // blockquote off + if (this.opts.linebreaks) + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else + { + var $el = this.utils.replaceToTag(block, 'p'); + this.block.toggle($el); + } + } + else if (block.tagName == 'LI') + { + if (tag != 'blockquote') return; - }, - setFullpageOnInit: function(html) - { - this.fullpageDoctype = html.match(/^<\!doctype[^>]*>/i); - if (this.fullpageDoctype && this.fullpageDoctype.length == 1) - { - html = html.replace(/^<\!doctype[^>]*>/i, ''); - } + this.block.formatListToBlockquote(); + } + else if (this.block.containerTag == 'BLOCKQUOTE') + { + this.block.formatBlockquote(tag); + } + else if (this.opts.linebreaks && ((isContainerTable) || (this.range.commonAncestorContainer != block))) + { + this.block.formatWrap(tag); + } + else + { + if (this.opts.linebreaks && tag == 'p') + { + $(block).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(block); + } + else if (block.tagName === 'TD') + { + this.block.formatWrap(tag); + } + else + { + var $formatted = this.utils.replaceToTag(block, tag); - html = this.cleanSavePreCode(html, true); - html = this.cleanConverters(html); - html = this.cleanEmpty(html); + this.block.toggle($formatted); - // set code - this.$editor.html(html); + if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted); + if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap(); + } + } + } + else + { + if (this.opts.linebreaks || tag != 'p') + { + if (tag == 'blockquote') + { + var count = 0; + for (var i = 0; i < this.block.blocksSize; i++) + { + if (this.block.blocks[i].tagName == 'BLOCKQUOTE') count++; + } - // set no editable - this.setNonEditable(); - this.setSpansVerified(); - this.sync(); - }, - setFullpageDoctype: function() - { - if (this.fullpageDoctype && this.fullpageDoctype.length == 1) - { - var source = this.fullpageDoctype[0] + '\n' + this.$source.val(); - this.$source.val(source); - } - }, - setSpansVerified: function() - { - var spans = this.$editor.find('span'); - var replacementTag = 'inline'; + // only blockquote selected + if (count == this.block.blocksSize) + { + $.each(this.block.blocks, $.proxy(function(i,s) + { + if (this.opts.linebreaks) + { + $(s).prepend('<br>').append('<br>'); + this.utils.replaceWithContents(s); + } + else + { + this.utils.replaceToTag(s, 'p'); + } - $.each(spans, function() { - var outer = this.outerHTML; + }, this)); - // Replace opening tag - var regex = new RegExp('<' + this.tagName, 'gi'); - var newTag = outer.replace(regex, '<' + replacementTag); + return; + } - // Replace closing tag - regex = new RegExp('</' + this.tagName, 'gi'); - newTag = newTag.replace(regex, '</' + replacementTag); + } - $(this).replaceWith(newTag); - }); + this.block.formatWrap(tag); + } + else + { + var classSize = 0; + var toggleType = false; + if (this.block.type == 'class') + { + toggleType = 'toggle'; + classSize = $(this.block.blocks).filter('.' + this.block.value).size(); - }, - setSpansVerifiedHtml: function(html) - { - html = html.replace(/<span(.*?)>/, '<inline$1>'); - return html.replace(/<\/span>/, '</inline>'); - }, - setNonEditable: function() - { - this.$editor.find('.noneditable').attr('contenteditable', false); - }, + if (this.block.blocksSize == classSize) toggleType = 'toggle'; + else if (this.block.blocksSize > classSize) toggleType = 'set'; + else if (classSize === 0) toggleType = 'set'; - // SYNC - sync: function(e) - { - var html = ''; + } - this.cleanUnverified(); + var exceptTags = ['ul', 'ol', 'li', 'td', 'th', 'dl', 'dt', 'dd']; + $.each(this.block.blocks, $.proxy(function(i,s) + { + if ($.inArray(s.tagName.toLowerCase(), exceptTags) != -1) return; - if (this.opts.fullpage) html = this.getCodeIframe(); - else html = this.$editor.html(); + var $formatted = this.utils.replaceToTag(s, tag); - html = this.syncClean(html); - html = this.cleanRemoveEmptyTags(html); + if (toggleType) + { + if (toggleType == 'toggle') this.block.toggle($formatted); + else if (toggleType == 'remove') this.block.remove($formatted); + else if (toggleType == 'set') this.block.setForce($formatted); + } + else this.block.toggle($formatted); - // is there a need to synchronize - var source = this.cleanRemoveSpaces(this.$source.val(), false); - var editor = this.cleanRemoveSpaces(html, false); + 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 (source == editor) - { - // do not sync - return false; - } - // fix second level up ul, ol - html = html.replace(/<\/li><(ul|ol)>([\w\W]*?)<\/(ul|ol)>/gi, '<$1>$2</$1></li>'); - if ($.trim(html) === '<br>') html = ''; + }, this)); + } + } + }, + setForce: function($el) + { + // remove style and class if the specified setting + if (this.block.clearStyle) + { + $el.removeAttr('class').removeAttr('style'); + } - // xhtml - if (this.opts.xhtml) - { - var xhtmlTags = ['br', 'hr', 'img', 'link', 'input', 'meta']; - $.each(xhtmlTags, function(i,s) + if (this.block.type == 'class') + { + $el.addClass(this.block.value); + return; + } + else if (this.block.type == 'attr' || this.block.type == 'data') + { + $el.attr(this.block.value.name, this.block.value.value); + return; + } + }, + toggle: function($el) { - html = html.replace(new RegExp('<' + s + '(.*?[^\/$]?)>', 'gi'), '<' + s + '$1 />'); - }); + // remove style and class if the specified setting + if (this.block.clearStyle) + { + $el.removeAttr('class').removeAttr('style'); + } - } + if (this.block.type == 'class') + { + $el.toggleClass(this.block.value); + return; + } + else if (this.block.type == 'attr' || this.block.type == 'data') + { + if ($el.attr(this.block.value.name) == this.block.value.value) + { + $el.removeAttr(this.block.value.name); + } + else + { + $el.attr(this.block.value.name, this.block.value.value); + } - // before callback - html = this.callback('syncBefore', false, html); + return; + } + else + { + $el.removeAttr('style class'); + return; + } + }, + remove: function($el) + { + $el.removeClass(this.block.value); + }, + formatListToBlockquote: function() + { + var block = $(this.block.blocks[0]).closest('ul, ol'); - this.$source.val(html); - this.setFullpageDoctype(); + $(block).find('ul, ol').contents().unwrap(); + $(block).find('li').append($('<br>')).contents().unwrap(); - // onchange & after callback - this.callback('syncAfter', false, html); + var $el = this.utils.replaceToTag(block, 'blockquote'); + this.block.toggle($el); + }, + formatBlockquote: function(tag) + { + document.execCommand('outdent'); + document.execCommand('formatblock', false, tag); - if (this.start === false) - { + this.clean.clearUnverified(); + this.$editor.find('p:empty').remove(); - if (typeof e != 'undefined') - { - switch(e.which) + var formatted = this.selection.getBlock(); + + if (tag != 'p') { - case 37: // left - break; - case 38: // up - break; - case 39: // right - break; - case 40: // down - break; + $(formatted).find('img').remove(); + } - default: this.callback('change', false, html); + if (!this.opts.linebreaks) + { + this.block.toggle($(formatted)); } - } - else - { - this.callback('change', false, html); - } - } - }, - syncClean: function(html) - { - if (!this.opts.fullpage) html = this.cleanStripTags(html); + this.$editor.find('ul, ol, tr, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); - // trim - html = $.trim(html); + if (this.opts.linebreaks && tag == 'p') + { + this.utils.replaceWithContents(formatted); + } - // removeplaceholder - html = this.placeholderRemoveFromCode(html); + }, + formatWrap: function(tag) + { + if (this.block.containerTag == 'UL' || this.block.containerTag == 'OL') + { + if (tag == 'blockquote') + { + this.block.formatListToBlockquote(); + } + else + { + return; + } + } - // remove space - html = html.replace(/&#x200b;/gi, ''); - html = html.replace(/&#8203;/gi, ''); - html = html.replace(/<\/a>&nbsp;/gi, '<\/a> '); - html = html.replace(/\u200B/g, ''); + var formatted = this.selection.wrap(tag); + if (formatted === false) return; - if (html == '<p></p>' || html == '<p> </p>' || html == '<p>&nbsp;</p>') - { - html = ''; - } + var $formatted = $(formatted); - // link nofollow - if (this.opts.linkNofollow) - { - html = html.replace(/<a(.*?)rel="nofollow"(.*?)>/gi, '<a$1$2>'); - html = html.replace(/<a(.*?)>/gi, '<a$1 rel="nofollow">'); - } + this.block.formatTableWrapping($formatted); - // php code fix - html = html.replace('<!--?php', '<?php'); - html = html.replace('?-->', '?>'); + var $elements = $formatted.find(this.opts.blockLevelElements.join(',') + ', td, table, thead, tbody, tfoot, th, tr'); - // revert no editable - html = html.replace(/<(.*?)class="noeditable"(.*?) contenteditable="false"(.*?)>/gi, '<$1class="noeditable"$2$3>'); + if ((this.opts.linebreaks && tag == 'p') || tag == 'pre' || tag == 'blockquote') + { + $elements.append('<br />'); + } - html = html.replace(/ data-tagblock=""/gi, ''); - html = html.replace(/<br\s?\/?>\n?<\/(P|H[1-6]|LI|ADDRESS|SECTION|HEADER|FOOTER|ASIDE|ARTICLE)>/gi, '</$1>'); + $elements.contents().unwrap(); - // 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, ''); + if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove(); - // remove empty lists - html = html.replace(/<(ul|ol)>\s*\t*\n*<\/(ul|ol)>/gi, ''); + $.each(this.block.blocks, $.proxy(this.utils.removeEmpty, this)); - // remove font - if (this.opts.cleanFontTag) - { - html = html.replace(/<font(.*?)>([\w\W]*?)<\/font>/gi, '$2'); - } + $formatted.append(this.selection.getMarker(2)); - // remove spans - html = html.replace(/<span(.*?)>([\w\W]*?)<\/span>/gi, '$2'); - html = html.replace(/<inline>([\w\W]*?)<\/inline>/gi, '$1'); - html = html.replace(/<inline>/gi, '<span>'); - html = html.replace(/<inline /gi, '<span '); - html = html.replace(/<\/inline>/gi, '</span>'); + if (!this.opts.linebreaks) + { + this.block.toggle($formatted); + } - if (this.opts.removeEmptyTags) - { - html = html.replace(/<span>([\w\W]*?)<\/span>/gi, '$1'); - } + this.$editor.find('ul, ol, tr, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); + $formatted.find('blockquote:empty').remove(); - html = html.replace(/<span(.*?)class="redactor_placeholder"(.*?)>([\w\W]*?)<\/span>/gi, ''); - html = html.replace(/<img(.*?)contenteditable="false"(.*?)>/gi, '<img$1$2>'); + if (this.block.isRemoveInline) + { + this.utils.removeInlineTags($formatted); + } - // special characters - html = html.replace(/&/gi, '&'); - html = html.replace(/\u2122/gi, '&trade;'); - html = html.replace(/\u00a9/gi, '&copy;'); - html = html.replace(/\u2026/gi, '&hellip;'); - html = html.replace(/\u2014/gi, '&mdash;'); - html = html.replace(/\u2010/gi, '&dash;'); + if (this.opts.linebreaks && tag == 'p') + { + this.utils.replaceWithContents($formatted); + } - html = this.cleanReConvertProtected(html); + }, + formatTableWrapping: function($formatted) + { + if ($formatted.closest('table').size() === 0) return; - return html; - }, + if ($formatted.closest('tr').size() === 0) $formatted.wrap('<tr>'); + if ($formatted.closest('td').size() === 0 && $formatted.closest('th').size() === 0) + { + $formatted.wrap('<td>'); + } + }, + removeData: function(name, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).removeAttr('data-' + name); + this.code.sync(); + }, + setData: function(name, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).attr('data-' + name, value); + this.code.sync(); + }, + toggleData: function(name, value) + { + var blocks = this.selection.getBlocks(); + $.each(blocks, function() + { + if ($(this).attr('data-' + name)) + { + $(this).removeAttr('data-' + name); + } + else + { + $(this).attr('data-' + name, value); + } + }); + }, + removeAttr: function(attr, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).removeAttr(attr); - // BUILD - buildStart: function() - { - // content - this.content = ''; + this.code.sync(); + }, + setAttr: function(attr, value) + { + var blocks = this.selection.getBlocks(); + $(blocks).attr(attr, value); - // container - this.$box = $('<div class="redactor_box" />'); + this.code.sync(); + }, + toggleAttr: function(attr, value) + { + var blocks = this.selection.getBlocks(); + $.each(blocks, function() + { + if ($(this).attr(name)) + { + $(this).removeAttr(name); + } + else + { + $(this).attr(name, value); + } + }); + }, + removeClass: function(className) + { + var blocks = this.selection.getBlocks(); + $(blocks).removeClass(className); - // textarea test - if (this.$source[0].tagName === 'TEXTAREA') this.opts.textareamode = true; + this.utils.removeEmptyAttr(blocks, 'class'); - // mobile - if (this.opts.mobile === false && this.isMobile()) - { - this.buildMobile(); - } - else - { - // get the content at the start - this.buildContent(); - - if (this.opts.iframe) + this.code.sync(); + }, + setClass: function(className) { - // build as iframe - this.opts.autoresize = false; - this.iframeStart(); - } - else if (this.opts.textareamode) this.buildFromTextarea(); - else this.buildFromElement(); + var blocks = this.selection.getBlocks(); + $(blocks).addClass(className); - // options and final setup - if (!this.opts.iframe) + this.code.sync(); + }, + toggleClass: function(className) { - this.buildOptions(); - this.buildAfter(); + var blocks = this.selection.getBlocks(); + $(blocks).toggleClass(className); + + this.code.sync(); } - } + }; }, - buildMobile: function() + buffer: function() { - if (!this.opts.textareamode) - { - this.$editor = this.$source; - this.$editor.hide(); - this.$source = this.buildCodearea(this.$editor); - this.$source.val(this.content); - } + return { + set: function(type) + { + if (typeof type == 'undefined' || type == 'undo') + { + this.buffer.setUndo(); + } + else + { + this.buffer.setRedo(); + } + }, + setUndo: function() + { + this.selection.save(); + this.opts.buffer.push(this.$editor.html()); + this.selection.restore(); + }, + setRedo: function() + { + this.selection.save(); + this.opts.rebuffer.push(this.$editor.html()); + this.selection.restore(); + }, + getUndo: function() + { + this.$editor.html(this.opts.buffer.pop()); + }, + getRedo: function() + { + this.$editor.html(this.opts.rebuffer.pop()); + }, + add: function() + { + this.opts.buffer.push(this.$editor.html()); + }, + undo: function() + { + if (this.opts.buffer.length === 0) return; - this.$box.insertAfter(this.$source).append(this.$source); - }, - buildContent: function() - { - if (this.opts.textareamode) this.content = $.trim(this.$source.val()); - else this.content = $.trim(this.$source.html()); - }, - buildFromTextarea: function() - { - this.$editor = $('<div />'); - this.$box.insertAfter(this.$source).append(this.$editor).append(this.$source); + this.buffer.set('redo'); + this.buffer.getUndo(); - // enable - this.buildAddClasses(this.$editor); - this.buildEnable(); - }, - buildFromElement: function() - { - this.$editor = this.$source; - this.$source = this.buildCodearea(this.$editor); - this.$box.insertAfter(this.$editor).append(this.$editor).append(this.$source); + this.selection.restore(); - // enable - this.buildEnable(); - }, - buildCodearea: function($source) - { - return $('<textarea />').attr('name', $source.attr('id')).css('height', this.sourceHeight); - }, - buildAddClasses: function(el) - { - // append textarea classes to editable layer - $.each(this.$source.get(0).className.split(/\s+/), function(i,s) - { - el.addClass('redactor_' + s); - }); - }, - buildEnable: function() - { - this.$editor.addClass('redactor_editor').attr({ 'contenteditable': true, 'dir': this.opts.direction }); - this.$source.attr('dir', this.opts.direction).hide(); + setTimeout($.proxy(this.observe.load, this), 50); + }, + redo: function() + { + if (this.opts.rebuffer.length === 0) return; - // set code - this.set(this.content, true, false); - }, - buildOptions: function() - { - var $source = this.$editor; - if (this.opts.iframe) $source = this.$frame; + this.buffer.set('undo'); + this.buffer.getRedo(); - // options - if (this.opts.tabindex) $source.attr('tabindex', this.opts.tabindex); + this.selection.restore(); - if (this.opts.minHeight) $source.css('min-height', this.opts.minHeight + 'px'); - // FF fix bug with line-height rendering - else if (this.browser('mozilla') && this.opts.linebreaks) - { - this.$editor.css('min-height', '45px'); - } - // FF fix bug with line-height rendering - if (this.browser('mozilla') && this.opts.linebreaks) - { - this.$editor.css('padding-bottom', '10px'); - } - - - if (this.opts.maxHeight) - { - this.opts.autoresize = false; - this.sourceHeight = this.opts.maxHeight; - } - if (this.opts.wym) this.$editor.addClass('redactor_editor_wym'); - if (this.opts.typewriter) this.$editor.addClass('redactor-editor-typewriter'); - if (!this.opts.autoresize) $source.css('height', this.sourceHeight); - + setTimeout($.proxy(this.observe.load, this), 50); + } + }; }, - buildAfter: function() + build: function() { - this.start = false; + return { + run: function() + { - // load toolbar - if (this.opts.toolbar) - { - this.opts.toolbar = this.toolbarInit(this.opts.curLang); - this.toolbarBuild(); - } + this.build.createContainerBox(); + this.build.loadContent(); + this.build.loadEditor(); + this.build.enableEditor(); + this.build.setCodeAndCall(); - // modal templates - this.modalTemplatesInit(); + }, + isTextarea: function() + { + return (this.$element[0].tagName === 'TEXTAREA'); + }, + createContainerBox: function() + { + this.$box = $('<div class="redactor-box" />'); + }, + 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; + } - // plugins - this.buildPlugins(); + return name; + }, + loadContent: function() + { + var func = (this.build.isTextarea()) ? 'val' : 'html'; + this.content = $.trim(this.$element[func]()); + }, + enableEditor: function() + { + this.$editor.attr({ 'contenteditable': true, 'dir': this.opts.direction }); + }, + loadEditor: function() + { + var func = (this.build.isTextarea()) ? 'fromTextarea' : 'fromElement'; + this.build[func](); + }, + fromTextarea: function() + { + this.$editor = $('<div />'); + this.$textarea = this.$element; + this.$box.insertAfter(this.$element).append(this.$editor).append(this.$element); + this.$editor.addClass('redactor-editor'); - // enter, tab, etc. - this.buildBindKeyboard(); + this.$element.hide(); + }, + fromElement: function() + { + this.$editor = this.$element; + this.build.createTextarea(); + this.$box.insertAfter(this.$editor).append(this.$editor).append(this.$textarea); + this.$editor.addClass('redactor-editor'); - // autosave - if (this.opts.autosave) this.autosave(); + this.$textarea.hide(); + }, + setCodeAndCall: function() + { + // set code + this.code.set(this.content); - // observers - setTimeout($.proxy(this.observeStart, this), 4); + this.build.setOptions(); + this.build.callEditor(); - // FF fix - if (this.browser('mozilla')) - { - try { - this.document.execCommand('enableObjectResizing', false, false); - this.document.execCommand('enableInlineTableEditing', false, false); - } catch (e) {} - } - - // focus - if (this.opts.focus) setTimeout($.proxy(this.focus, this), 100); - - // code mode - if (!this.opts.visual) - { - setTimeout($.proxy(function() + // code mode + if (!this.opts.visual) + { + setTimeout($.proxy(this.code.showCode, this), 200); + } + }, + callEditor: function() { - this.opts.visual = true; - this.toggle(false); + this.build.disableMozillaEditing(); + this.build.setEvents(); + this.build.setHelpers(); - }, this), 200); - } + // load toolbar + if (this.opts.toolbar) + { + this.opts.toolbar = this.toolbar.init(); + this.toolbar.build(); + } - // init callback - this.callback('init'); - }, - buildBindKeyboard: function() - { - this.dblEnter = 0; + // modal templates init + this.modal.loadTemplates(); - if (this.opts.dragUpload && (this.opts.imageUpload !== false || this.opts.s3 !== false)) - { - this.$editor.on('drop.redactor', $.proxy(this.buildEventDrop, this)); - } + // plugins + this.build.plugins(); - this.$editor.on('click.redactor', $.proxy(function() - { - this.selectall = false; + // observers + setTimeout($.proxy(this.observe.load, this), 4); - }, this)); + // init callback + this.core.setCallback('init'); + }, + setOptions: function() + { + // textarea direction + $(this.$textarea).attr('dir', this.opts.direction); - this.$editor.on('input.redactor', $.proxy(this.sync, this)); - this.$editor.on('paste.redactor', $.proxy(this.buildEventPaste, this)); - this.$editor.on('keydown.redactor', $.proxy(this.buildEventKeydown, this)); - this.$editor.on('keyup.redactor', $.proxy(this.buildEventKeyup, this)); + if (this.opts.linebreaks) this.$editor.addClass('redactor-linebreaks'); - // textarea callback - if ($.isFunction(this.opts.textareaKeydownCallback)) - { - this.$source.on('keydown.redactor-textarea', $.proxy(this.opts.textareaKeydownCallback, this)); - } + if (this.opts.tabindex) this.$editor.attr('tabindex', this.opts.tabindex); - // focus callback - if ($.isFunction(this.opts.focusCallback)) - { - this.$editor.on('focus.redactor', $.proxy(this.opts.focusCallback, this)); - } + if (this.opts.minHeight) this.$editor.css('minHeight', this.opts.minHeight); + if (this.opts.maxHeight) this.$editor.css('maxHeight', this.opts.maxHeight); - var clickedElement; - $(document).mousedown(function(e) { - clickedElement = $(e.target); - }); - - // blur callback - this.$editor.on('blur.redactor', $.proxy(function(e) - { - if (!$(clickedElement).hasClass('redactor_toolbar') && $(clickedElement).parents('.redactor_toolbar').size() == 0) + }, + setEvents: function() { - this.selectall = false; - if ($.isFunction(this.opts.blurCallback)) this.callback('blur', e); - } - }, this)); + // drop + this.$editor.on('drop.redactor', $.proxy(function(e) + { + e = e.originalEvent || e; - }, - buildEventDrop: function(e) - { - e = e.originalEvent || e; + if (window.FormData === undefined || !e.dataTransfer) return true; - if (window.FormData === undefined || !e.dataTransfer) return true; + var length = e.dataTransfer.files.length; + if (length === 0) + { + this.code.sync(); + setTimeout($.proxy(this.clean.clearUnverified, this), 1); + this.core.setCallback('drop', e); - var length = e.dataTransfer.files.length; - if (length == 0) return true; + return true; + } + else + { + e.preventDefault(); - e.preventDefault(); + if (this.opts.dragImageUpload || this.opts.dragFileUpload) + { + var files = e.dataTransfer.files; + this.upload.directUpload(files[0], e); + } + } - var file = e.dataTransfer.files[0]; + setTimeout($.proxy(this.clean.clearUnverified, this), 1); - if (this.opts.dnbImageTypes !== false && this.opts.dnbImageTypes.indexOf(file.type) == -1) - { - return true; - } + this.core.setCallback('drop', e); - this.bufferSet(); + }, this)); - this.showProgressBar(); - if (this.opts.s3 === false) - { - this.dragUploadAjax(this.opts.imageUpload, file, true, e, this.opts.imageUploadParam); - } - else - { - this.s3uploadFile(file); - } + // click + this.$editor.on('click.redactor', $.proxy(function(e) + { + var type = 'click'; + if ((this.core.getEvent() == 'click' || this.core.getEvent() == 'arrow')) + { + type = false; + } + this.core.addEvent(type); + this.utils.disableSelectAll(); + this.core.setCallback('click', e); - }, - buildEventPaste: function(e) - { - var oldsafari = false; - if (this.browser('webkit') && navigator.userAgent.indexOf('Chrome') === -1) - { - var arr = this.browser('version').split('.'); - if (arr[0] < 536) oldsafari = true; - } + }, this)); - if (oldsafari) return true; + // paste + this.$editor.on('paste.redactor', $.proxy(this.paste.init, this)); - // paste except opera (not webkit) - if (this.browser('opera')) return true; + // keydown + this.$editor.on('keydown.redactor', $.proxy(this.keydown.init, this)); - // clipboard upload - if (this.opts.clipboardUpload && this.buildEventClipboardUpload(e)) return true; + // keyup + this.$editor.on('keyup.redactor', $.proxy(this.keyup.init, this)); - if (this.opts.cleanup) - { - this.rtePaste = true; + // textarea keydown + if ($.isFunction(this.opts.codeKeydownCallback)) + { + this.$textarea.on('keydown.redactor-textarea', $.proxy(this.opts.codeKeydownCallback, this)); + } - this.selectionSave(); - - if (!this.selectall) - { - if (this.opts.autoresize === true && this.fullscreen !== true) + // textarea keyup + if ($.isFunction(this.opts.codeKeyupCallback)) { - this.$editor.height(this.$editor.height()); - this.saveScroll = this.document.body.scrollTop; + this.$textarea.on('keyup.redactor-textarea', $.proxy(this.opts.codeKeyupCallback, this)); } - else + + // focus + if ($.isFunction(this.opts.focusCallback)) { - this.saveScroll = this.$editor.scrollTop(); + this.$editor.on('focus.redactor', $.proxy(this.opts.focusCallback, this)); } - } - var frag = this.extractContent(); + var clickedElement; + $(document).on('mousedown', function(e) { + clickedElement = $(e.target); + }); - setTimeout($.proxy(function() + // blur + this.$editor.on('blur.redactor', $.proxy(function(e) + { + 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) + { + this.utils.disableSelectAll(); + if ($.isFunction(this.opts.blurCallback)) this.core.setCallback('blur', e); + } + }, this)); + }, + setHelpers: function() { - var pastedFrag = this.extractContent(); - this.$editor.append(frag); + // autosave + this.autosave.enable(); - this.selectionRestore(); + // placeholder + this.placeholder.enable(); - var html = this.getFragmentHtml(pastedFrag); - this.pasteClean(html); + // 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.autoresize === true && this.fullscreen !== true) this.$editor.css('height', 'auto'); - - }, this), 1); - } - }, - buildEventClipboardUpload: function(e) - { - var event = e.originalEvent || e; - this.clipboardFilePaste = false; - - - if (typeof(event.clipboardData) === 'undefined') return false; - if (event.clipboardData.items) - { - var file = event.clipboardData.items[0].getAsFile(); - if (file !== null) + }, + plugins: function() { - this.bufferSet(); - this.clipboardFilePaste = true; + if (!this.opts.plugins) return; + if (!RedactorPlugins) return; - var reader = new FileReader(); - reader.onload = $.proxy(this.pasteClipboardUpload, this); - reader.readAsDataURL(file); + $.each(this.opts.plugins, $.proxy(function(i, s) + { + if (typeof RedactorPlugins[s] === 'undefined') return; - return true; - } - } + if ($.inArray(s, $.Redactor.modules) !== -1) + { + $.error('Plugin name "' + s + '" matches the name of the Redactor\'s module.'); + return; + } - return false; + if (!$.isFunction(RedactorPlugins[s])) return; - }, - buildEventKeydown: function(e) - { - if (this.rtePaste) return false; + this[s] = RedactorPlugins[s](); - var key = e.which; - var ctrl = e.ctrlKey || e.metaKey; - var parent = this.getParent(); - var current = this.getCurrent(); - var block = this.getBlock(); - var pre = false; + var methods = this.getModuleMethods(this[s]); + var len = methods.length; - this.callback('keydown', e); - - /* - firefox cmd+left/Cmd+right browser back/forward fix - - http://joshrhoderick.wordpress.com/2010/05/05/how-firefoxs-command-key-bug-kills-usability-on-the-mac/ - */ - if (this.browser('mozilla') && "modify" in window.getSelection()) - { - if ((ctrl) && (e.keyCode===37 || e.keyCode===39)) - { - var selection = this.getSelection(); - var lineOrWord = (e.metaKey ? "line" : "word"); - if (e.keyCode===37) - { - selection.modify("extend","left",lineOrWord); - if (!e.shiftKey) + // bind methods + for (var z = 0; z < len; z++) { - selection.collapseToStart(); + this[s][methods[z]] = this[s][methods[z]].bind(this); } - } - if (e.keyCode===39) - { - selection.modify("extend","right",lineOrWord); - if (!e.shiftKey) - { - selection.collapseToEnd(); - } - } - e.preventDefault(); - } - } + if ($.isFunction(this[s].init)) this[s].init(); - this.imageResizeHide(false); + }, this)); - // pre & down - if ((parent && $(parent).get(0).tagName === 'PRE') || (current && $(current).get(0).tagName === 'PRE')) - { - pre = true; - if (key === this.keyCode.DOWN) this.insertAfterLastElement(block); - } - // down - if (key === this.keyCode.DOWN) - { - if (parent && $(parent)[0].tagName === 'BLOCKQUOTE') this.insertAfterLastElement(parent); - if (current && $(current)[0].tagName === 'BLOCKQUOTE') this.insertAfterLastElement(current); - - if (parent && $(parent)[0].tagName === 'P' && $(parent).parent()[0].tagName == 'BLOCKQUOTE') + }, + disableMozillaEditing: function() { - this.insertAfterLastElement(parent, $(parent).parent()[0]); - } - if (current && $(current)[0].tagName === 'P' && parent && $(parent)[0].tagName == 'BLOCKQUOTE') - { - this.insertAfterLastElement(current, parent); - } - } + if (!this.utils.browser('mozilla')) return; - // shortcuts setup - this.shortcuts(e, key); - - // buffer setup - if (ctrl && key === 90 && !e.shiftKey && !e.altKey) // z key - { - e.preventDefault(); - if (this.opts.buffer.length) this.bufferUndo(); - else this.document.execCommand('undo', false, false); - return; - } - // undo - else if (ctrl && key === 90 && e.shiftKey && !e.altKey) - { - e.preventDefault(); - if (this.opts.rebuffer.length != 0) this.bufferRedo(); - else this.document.execCommand('redo', false, false); - return; - } - - // space - if (key == 32) - { - this.bufferSet(); - } - - // select all - if (ctrl && key === 65) - { - this.bufferSet(); - this.selectall = true; - } - else if (key != this.keyCode.LEFT_WIN && !ctrl) - { - this.selectall = false; - } - - // enter - if (key == this.keyCode.ENTER && !e.shiftKey && !e.ctrlKey && !e.metaKey) - { - // remove selected content on enter - var range = this.getRange(); - if (range && range.collapsed === false) - { - sel = this.getSelection(); - if (sel.rangeCount) - { - range.deleteContents(); - } + // FF fix + try { + document.execCommand('enableObjectResizing', false, false); + document.execCommand('enableInlineTableEditing', false, false); + } catch (e) {} } - - // In ie, opera in the tables are created paragraphs, fix it. - if (this.browser('msie') && (parent.nodeType == 1 && (parent.tagName == 'TD' || parent.tagName == 'TH'))) + }; + }, + button: function() + { + return { + build: function(btnName, btnObject) { - e.preventDefault(); - this.bufferSet(); - this.insertNode(document.createElement('br')); - this.callback('enter', e); - return false; - } + var $button = $('<a href="#" class="re-icon re-' + btnName + '" rel="' + btnName + '" />').attr('tabindex', '-1'); - // blockquote exit - if (block && (block.tagName == 'BLOCKQUOTE' || $(block).parent()[0].tagName == 'BLOCKQUOTE')) - { - if (this.isEndOfElement()) + if (btnObject.func || btnObject.command || btnObject.dropdown) { - if (this.dblEnter == 1) + $button.on('touchstart click', $.proxy(function(e) { - var element; - var last; - if (block.tagName == 'BLOCKQUOTE') + if ($button.hasClass('redactor-button-disabled')) return false; + + var type = 'func'; + var callback = btnObject.func; + if (btnObject.command) { - last = 'br'; - element = block; + type = 'command'; + callback = btnObject.command; } - else + else if (btnObject.dropdown) { - last = 'p'; - element = $(block).parent()[0]; + type = 'dropdown'; + callback = false; } - e.preventDefault(); - this.insertingAfterLastElement(element); - this.dblEnter = 0; + this.button.onClick(e, btnName, type, callback); - if (last == 'p') - { - $(block).parent().find('p').last().remove(); - } - else - { - var tmp = $.trim($(block).html()); - $(block).html(tmp.replace(/<br\s?\/?>$/i, '')); - } + }, this)); + } - return; - } - else this.dblEnter++; + // dropdown + if (btnObject.dropdown) + { + var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-box-' + btnName + '" style="display: none;">'); + $button.data('dropdown', $dropdown); + this.dropdown.build(btnName, $dropdown, btnObject.dropdown); } - else this.dblEnter++; - } - // pre - if (pre === true) + // tooltip + if (this.utils.isDesktop()) + { + this.button.createTooltip($button, btnName, btnObject.title); + } + + return $button; + }, + createTooltip: function($button, name, title) { - return this.buildEventKeydownPre(e, current); - } - else - { - if (!this.opts.linebreaks) + var $tooltip = $('<span>').addClass('redactor-toolbar-tooltip redactor-toolbar-tooltip-' + name).hide().html(title); + $tooltip.appendTo('body'); + + $button.on('mouseover', function() { - // lists exit - if (block && block.tagName == 'LI') - { - var listCurrent = this.getBlock(); - if (listCurrent !== false || listCurrent.tagName === 'LI') - { - var listText = $.trim($(block).text()); - var listCurrentText = $.trim($(listCurrent).text()); - if (listText == '' - && listCurrentText == '' - && $(listCurrent).next('li').size() == 0 - && $(listCurrent).parents('li').size() == 0) - { - this.bufferSet(); + if ($(this).hasClass('redactor-button-disabled')) return; - var $list = $(listCurrent).closest('ol, ul'); - $(listCurrent).remove(); - var node = $('<p>' + this.opts.invisibleSpace + '</p>'); - $list.after(node); - this.selectionStart(node); + var pos = $button.offset(); + var height = $button.innerHeight(); + var width = $button.innerWidth(); - this.sync(); - this.callback('enter', e); - return false; - } - } + $tooltip.show(); + $tooltip.css({ + top: (pos.top + height) + 'px', + left: (pos.left + width/2 - $tooltip.innerWidth()/2) + 'px' + }); + }); - } + $button.on('mouseout', function() + { + $tooltip.hide(); + }); - // replace div to p - if (block && this.opts.rBlockTest.test(block.tagName)) - { - // hit enter - this.bufferSet(); + }, + onClick: function(e, btnName, type, callback) + { + this.button.caretOffset = this.caret.getOffset(); - setTimeout($.proxy(function() - { - var blockElem = this.getBlock(); - if (blockElem.tagName === 'DIV' && !$(blockElem).hasClass('redactor_editor')) - { - var node = $('<p>' + this.opts.invisibleSpace + '</p>'); - $(blockElem).replaceWith(node); - this.selectionStart(node); - } + e.preventDefault(); - }, this), 1); - } - else if (block === false) - { - // hit enter - this.bufferSet(); - var node = $('<p>' + this.opts.invisibleSpace + '</p>'); - this.insertNode(node[0]); - this.selectionStart(node); - this.callback('enter', e); - return false; - } + if (this.utils.browser('msie')) e.returnValue = false; + if (type == 'command') + { + this.inline.format(callback); } - - if (this.opts.linebreaks) + else if (type == 'dropdown') { - // replace div to br - if (block && this.opts.rBlockTest.test(block.tagName)) - { - // hit enter - this.bufferSet(); + this.dropdown.show(e, btnName); + } + else + { + var func; - setTimeout($.proxy(function() + 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') { - var blockElem = this.getBlock(); - if ((blockElem.tagName === 'DIV' || blockElem.tagName === 'P') && !$(blockElem).hasClass('redactor_editor')) - { - this.replaceLineBreak(blockElem); - } - - }, this), 1); + this[func[0]][func[1]](btnName); + this.observe.buttons(e, btnName); + } } else { - return this.buildEventKeydownInsertLineBreak(e); + this[callback](btnName); + this.observe.buttons(e, btnName); } } - - // blockquote, figcaption - if (block.tagName == 'BLOCKQUOTE' || block.tagName == 'FIGCAPTION') + }, + get: function(key) + { + return this.$toolbar.find('a.re-' + key); + }, + setActive: function(key) + { + this.button.get(key).addClass('redactor-act'); + }, + setInactive: function(key) + { + this.button.get(key).removeClass('redactor-act'); + }, + setInactiveAll: function(key) + { + if (typeof key == 'undefined') { - return this.buildEventKeydownInsertLineBreak(e); + 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'); + }, + setInactiveInCode: function() + { + this.$toolbar.find('a.re-icon').not('a.re-html').addClass('redactor-button-disabled'); + }, + changeIcon: function(key, classname) + { + this.button.get(key).addClass('re-' + classname); + }, + removeIcon: function(key, classname) + { + this.button.get(key).removeClass('re-' + classname); + }, + setAwesome: function(key, name) + { + var $button = this.button.get(key); + $button.removeClass('redactor-btn-image').addClass('fa-redactor-btn'); + $button.html('<i class="fa ' + name + '"></i>'); + }, + addCallback: function($btn, callback) + { + 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; + this.button.onClick(e, key, type, callback); - } + }, this)); + }, + addDropdown: function($btn, dropdown) + { + var key = $btn.attr('rel'); + this.button.addCallback($btn, 'dropdown'); - this.callback('enter', e); - } - else if (key === this.keyCode.ENTER && (e.ctrlKey || e.shiftKey)) // Shift+Enter or Ctrl+Enter - { - this.bufferSet(); + var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-box-' + key + '" style="display: none;">'); + $btn.data('dropdown', $dropdown); - e.preventDefault(); - this.insertLineBreak(); - } + if (dropdown) + { + this.dropdown.build(key, $dropdown, dropdown); + } - // tab (cmd + [) - if ((key === this.keyCode.TAB || e.metaKey && key === 219) && this.opts.shortcuts) - { - return this.buildEventKeydownTab(e, pre, key); - } + return $dropdown; + }, + add: function(key, title) + { + if (!this.opts.toolbar) return; - // delete zero-width space before the removing - if (key === this.keyCode.BACKSPACE) this.buildEventKeydownBackspace(e, current, parent); + var btn = this.button.build(key, { title: title }); + btn.addClass('redactor-btn-image'); - }, - buildEventKeydownPre: function(e, current) - { - e.preventDefault(); - this.bufferSet(); - var html = $(current).parent().text(); - this.insertNode(document.createTextNode('\n')); - if (html.search(/\s$/) == -1) - { - this.insertNode(document.createTextNode('\n')); - } + this.$toolbar.append($('<li>').append(btn)); - this.sync(); - this.callback('enter', e); - return false; - }, - buildEventKeydownTab: function(e, pre, key) - { - if (!this.opts.tabFocus) return true; - if (this.isEmpty(this.get()) && this.opts.tabSpaces === false) return true; + return btn; + }, + addFirst: function(key, title) + { + if (!this.opts.toolbar) return; - e.preventDefault(); + var btn = this.button.build(key, { title: title }); + this.$toolbar.prepend($('<li>').append(btn)); - if (pre === true && !e.shiftKey) - { - this.bufferSet(); - this.insertNode(document.createTextNode('\t')); - this.sync(); - return false; + return btn; + }, + addAfter: function(afterkey, key, title) + { + if (!this.opts.toolbar) return; - } - else if (this.opts.tabSpaces !== false) - { - this.bufferSet(); - this.insertNode(document.createTextNode(Array(this.opts.tabSpaces + 1).join('\u00a0'))); - this.sync(); - return false; - } - else - { - if (!e.shiftKey) this.indentingIndent(); - else this.indentingOutdent(); - } + var btn = this.button.build(key, { title: title }); + var $btn = this.button.get(afterkey); - return false; - }, - buildEventKeydownBackspace: function(e, current, parent) - { - // remove empty list in table - if (parent && current && parent.parentNode.tagName == 'TD' - && parent.tagName == 'UL' && current.tagName == 'LI' && $(parent).children('li').size() == 1) - { - var text = $(current).text().replace(/[\u200B-\u200D\uFEFF]/g, ''); - if (text == '') + if ($btn.size() !== 0) $btn.parent().after($('<li>').append(btn)); + else this.$toolbar.append($('<li>').append(btn)); + + return btn; + }, + addBefore: function(beforekey, key, title) { - var node = parent.parentNode; - $(parent).remove(); - this.selectionStart(node); - this.sync(); - return false; - } - } + if (!this.opts.toolbar) return; - if (typeof current.tagName !== 'undefined' && /^(H[1-6])$/i.test(current.tagName)) - { - var node; - if (this.opts.linebreaks === false) node = $('<p>' + this.opts.invisibleSpace + '</p>'); - else node = $('<br>' + this.opts.invisibleSpace); + var btn = this.button.build(key, { title: title }); + var $btn = this.button.get(beforekey); - $(current).replaceWith(node); - this.selectionStart(node); - this.sync(); - } + if ($btn.size() !== 0) $btn.parent().before($('<li>').append(btn)); + else this.$toolbar.append($('<li>').append(btn)); - if (typeof current.nodeValue !== 'undefined' && current.nodeValue !== null) - { - if (current.remove && current.nodeType === 3 && current.nodeValue.match(/[^\u200B]/g) == null) + return btn; + }, + remove: function(key) { - $(current).prev().remove(); - this.sync(); + this.button.get(key).remove(); } - } + }; }, - buildEventKeydownInsertLineBreak: function(e) + caret: function() { - this.bufferSet(); - e.preventDefault(); - this.insertLineBreak(); - this.callback('enter', e); - return; - }, - buildEventKeyup: function(e) - { - if (this.rtePaste) return false; + return { + setStart: function(node) + { + // inline tag + if (!this.utils.isBlock(node)) + { + var space = this.utils.createSpaceElement(); - var key = e.which; - var parent = this.getParent(); - var current = this.getCurrent(); - - // replace to p before / after the table or body - if (!this.opts.linebreaks && current.nodeType == 3 && (parent == false || parent.tagName == 'BODY')) - { - var node = $('<p>').append($(current).clone()); - $(current).replaceWith(node); - var next = $(node).next(); - if (typeof(next[0]) !== 'undefined' && next[0].tagName == 'BR') + $(node).prepend(space); + this.caret.setEnd(space); + } + else + { + this.caret.set(node, 0, node, 0); + } + }, + setEnd: function(node) { - next.remove(); - } + this.caret.set(node, 1, node, 1); + }, + set: function(orgn, orgo, focn, foco) + { + // focus + if (!this.utils.browser('msie')) this.$editor.focus(); - this.selectionEnd(node); - } + orgn = orgn[0] || orgn; + focn = focn[0] || focn; - // convert links - if ((this.opts.convertLinks || this.opts.convertImageLinks || this.opts.convertVideoLinks) && key === this.keyCode.ENTER) - { - this.buildEventKeyupConverters(); - } + if (this.utils.isBlockTag(orgn.tagName) && orgn.innerHTML === '') + { + orgn.innerHTML = this.opts.invisibleSpace; + } - // if empty - if (key === this.keyCode.DELETE || key === this.keyCode.BACKSPACE) - { - return this.formatEmpty(e); - } + if (orgn.tagName == 'BR' && this.opts.linebreaks === false) + { + var par = $(this.opts.emptyHtml)[0]; + $(orgn).replaceWith(par); + orgn = par; + focn = orgn; + } - this.callback('keyup', e); - this.sync(e); - }, - buildEventKeyupConverters: function() - { - this.formatLinkify(this.opts.linkProtocol, this.opts.convertLinks, this.opts.convertImageLinks, this.opts.convertVideoLinks, this.opts.linkSize); + this.selection.get(); - setTimeout($.proxy(function() - { - if (this.opts.convertImageLinks) this.observeImages(); - if (this.opts.observeLinks) this.observeLinks(); - }, this), 5); - }, - buildPlugins: function() - { - if (!this.opts.plugins ) return; + try { + this.range.setStart(orgn, orgo); + this.range.setEnd(focn, foco); + } + catch (e) {} - $.each(this.opts.plugins, $.proxy(function(i, s) - { - if (RedactorPlugins[s]) + this.selection.addRange(); + }, + setAfter: function(node) { - $.extend(this, RedactorPlugins[s]); - if ($.isFunction( RedactorPlugins[ s ].init)) this.init(); - } + try { + var tag = $(node)[0].tagName; - }, this )); - }, + // inline tag + if (tag != 'BR' && !this.utils.isBlock(node)) + { + var space = this.utils.createSpaceElement(); - // IFRAME - iframeStart: function() - { - this.iframeCreate(); - - if (this.opts.textareamode) this.iframeAppend(this.$source); - else - { - this.$sourceOld = this.$source.hide(); - this.$source = this.buildCodearea(this.$sourceOld); - this.iframeAppend(this.$sourceOld); - } - }, - iframeAppend: function(el) - { - this.$source.attr('dir', this.opts.direction).hide(); - this.$box.insertAfter(el).append(this.$frame).append(this.$source); - }, - iframeCreate: function() - { - this.$frame = $('<iframe style="width: 100%;" frameborder="0" />').one('load', $.proxy(function() - { - if (this.opts.fullpage) + $(node).after(space); + this.caret.setEnd(space); + } + else + { + if (tag != 'BR' && this.utils.browser('msie')) + { + this.caret.setStart($(node).next()); + } + else + { + this.caret.setAfterOrBefore(node, 'after'); + } + } + } + catch (e) { + var space = this.utils.createSpaceElement(); + $(node).after(space); + this.caret.setEnd(space); + } + }, + setBefore: function(node) { - this.iframePage(); + // block tag + if (this.utils.isBlock(node)) + { + this.caret.setEnd($(node).prev()); + } + else + { + this.caret.setAfterOrBefore(node, 'before'); + } + }, + setAfterOrBefore: function(node, type) + { + // focus + if (!this.utils.browser('msie')) this.$editor.focus(); - if (this.content === '') this.content = this.opts.invisibleSpace; + node = node[0] || node; - this.$frame.contents()[0].write(this.content); - this.$frame.contents()[0].close(); + this.selection.get(); - var timer = setInterval($.proxy(function() + if (type == 'after') { - if (this.$frame.contents().find('body').html()) - { - clearInterval(timer); - this.iframeLoad(); + try { + + this.range.setStartAfter(node); + this.range.setEndAfter(node); } + catch (e) {} + } + else + { + try { + this.range.setStartBefore(node); + this.range.setEndBefore(node); + } + catch (e) {} + } - }, this), 0); - } - else this.iframeLoad(); - }, this)); - }, - iframeDoc: function() - { - return this.$frame[0].contentWindow.document; - }, - iframePage: function() - { - var doc = this.iframeDoc(); - if (doc.documentElement) doc.removeChild(doc.documentElement); + this.range.collapse(false); + this.selection.addRange(); + }, + getOffsetOfElement: function(node) + { + node = node[0] || node; - return doc; - }, - iframeAddCss: function(css) - { - css = css || this.opts.css; + this.selection.get(); - if (this.isString(css)) - { - this.$frame.contents().find('head').append('<link rel="stylesheet" href="' + css + '" />'); - } + var cloned = this.range.cloneRange(); + cloned.selectNodeContents(node); + cloned.setEnd(this.range.endContainer, this.range.endOffset); - if ($.isArray(css)) - { - $.each(css, $.proxy(function(i, url) + return $.trim(cloned.toString()).length; + }, + getOffset: function() { - this.iframeAddCss(url); + var offset = 0; + var sel = window.getSelection(); - }, this)); - } - }, - iframeLoad: function() - { - this.$editor = this.$frame.contents().find('body').attr({ 'contenteditable': true, 'dir': this.opts.direction }); + if (sel.rangeCount > 0) + { + var range = window.getSelection().getRangeAt(0); + var caretRange = range.cloneRange(); + caretRange.selectNodeContents(this.$editor[0]); + caretRange.setEnd(range.endContainer, range.endOffset); + offset = caretRange.toString().length; + } - // set document & window - if (this.$editor[0]) - { - this.document = this.$editor[0].ownerDocument; - this.window = this.document.defaultView || window; - } + return offset; + }, + setOffset: function(start, end) + { + if (typeof end == 'undefined') end = start; + if (!this.focus.isFocused()) this.focus.setStart(); - // iframe css - this.iframeAddCss(); + var sel = this.selection.get(); + var node, offset = 0; + var walker = document.createTreeWalker(this.$editor[0], NodeFilter.SHOW_TEXT, null, null); - if (this.opts.fullpage) - { - this.setFullpageOnInit(this.$source.val()); - } - else this.set(this.content, true, false); + while (node = walker.nextNode()) + { + offset += node.nodeValue.length; + if (offset > start) + { + this.range.setStart(node, node.nodeValue.length + start - offset); + start = Infinity; + } - this.buildOptions(); - this.buildAfter(); - }, + if (offset >= end) + { + this.range.setEnd(node, node.nodeValue.length + end - offset); + break; + } + } - // PLACEHOLDER - placeholderInit: function() - { - if (this.opts.placeholder !== false) - { - this.placeholderText = this.opts.placeholder; - this.opts.placeholder = true; - } - else - { - if (typeof this.$element.attr('placeholder') == 'undefined' || this.$element.attr('placeholder') == '') + this.range.collapse(false); + this.selection.addRange(); + }, + setToPoint: function(start, end) { - this.opts.placeholder = false; - } - else + this.caret.setOffset(start, end); + }, + getCoords: function() { - this.placeholderText = this.$element.attr('placeholder'); - this.opts.placeholder = true; + return this.caret.getOffset(); } - } + }; }, - placeholderStart: function(html) + clean: function() { - if (this.opts.placeholder === false) - { - return false; - } + return { + onSet: function(html) + { + html = this.clean.savePreCode(html); - if (this.isEmpty(html)) - { - this.opts.focus = false; - this.placeholderOnFocus(); - this.placeholderOnBlur(); + // convert script tag + html = html.replace(/<script(.*?[^>]?)>([\w\W]*?)<\/script>/gi, '<pre class="redactor-script-tag" style="display: none;" $1>$2</pre>'); - return this.placeholderGet(); - } - else - { - this.placeholderOnBlur(); - } + // replace dollar sign to entity + html = html.replace(/\$/g, '&#36;'); + html = html.replace(/”/g, '"'); + html = html.replace(/‘/g, '\''); + html = html.replace(/’/g, '\''); - return false; - }, - placeholderOnFocus: function() - { - this.$editor.on('focus.redactor_placeholder', $.proxy(this.placeholderFocus, this)); - }, - placeholderOnBlur: function() - { - this.$editor.on('blur.redactor_placeholder', $.proxy(this.placeholderBlur, this)); - }, - placeholderGet: function() - { - var ph = $('<span class="redactor_placeholder">').data('redactor', 'verified') - .attr('contenteditable', false).text(this.placeholderText); + if (this.opts.replaceDivs) html = this.clean.replaceDivs(html); + if (this.opts.linebreaks) html = this.clean.replaceParagraphsToBr(html); - if (this.opts.linebreaks === false) - { - return $('<p>').append(ph); - } - else return ph; - }, - placeholderBlur: function() - { - var html = this.get(); - if (this.isEmpty(html)) - { - this.placeholderOnFocus(); - this.$editor.html(this.placeholderGet()); - } - }, - placeholderFocus: function() - { - this.$editor.find('span.redactor_placeholder').remove(); + // save form tag + html = this.clean.saveFormTags(html); - var html = ''; - if (this.opts.linebreaks === false) - { - html = this.opts.emptyHtml; - } + // convert font tag to span + var $div = $('<div>'); + $div.html(html); + var fonts = $div.find('font[style]'); + if (fonts.length !== 0) + { + fonts.replaceWith(function() + { + var $el = $(this); + var $span = $('<span>').attr('style', $el.attr('style')); + return $span.append($el.contents()); + }); - this.$editor.off('focus.redactor_placeholder'); - this.$editor.html(html); + html = $div.html(); + } + $div.remove(); - if (this.opts.linebreaks === false) - { - // place the cursor inside emptyHtml - this.selectionStart(this.$editor.children()[0]); - } - else - { - this.focus(); - } + // remove font tag + html = html.replace(/<font(.*?[^<])>/gi, ''); + html = html.replace(/<\/font>/gi, ''); - this.sync(); - }, - placeholderRemoveFromEditor: function() - { - this.$editor.find('span.redactor_placeholder').remove(); - this.$editor.off('focus.redactor_placeholder'); - }, - placeholderRemoveFromCode: function(html) - { - return html.replace(/<span class="redactor_placeholder"(.*?)>(.*?)<\/span>/i, ''); - }, + // tidy html + html = this.tidy.load(html); - // SHORTCUTS - shortcuts: function(e, key) - { + // paragraphize + if (this.opts.paragraphize) html = this.paragraphize.load(html); - // disable browser's hot keys for bold and italic - if (!this.opts.shortcuts) - { - if ((e.ctrlKey || e.metaKey) && (key === 66 || key === 73)) - { - e.preventDefault(); - } + // verified + html = this.clean.setVerified(html); - return false; - } + // convert inline tags + html = this.clean.convertInline(html); - $.each(this.opts.shortcuts, $.proxy(function(str, command) - { - var keys = str.split(','); - for (var i in keys) + return html; + }, + onSync: function(html) { - if (typeof keys[i] === 'string') + // remove spaces + html = html.replace(/[\u200B-\u200D\uFEFF]/g, ''); + html = html.replace(/&#x200b;/gi, ''); + + if (this.opts.cleanSpaces) { - this.shortcutsHandler(e, $.trim(keys[i]), $.proxy(function() - { - eval(command); - }, this)); + html = html.replace(/&nbsp;/gi, ' '); } - } + if (html.search(/^<p>(||\s||<br\s?\/?>||&nbsp;)<\/p>$/i) != -1) + { + return ''; + } - }, this)); + // reconvert script tag + html = html.replace(/<pre class="redactor-script-tag" style="display: none;"(.*?[^>]?)>([\w\W]*?)<\/pre>/gi, '<script$1>$2</script>'); + // restore form tag + html = this.clean.restoreFormTags(html); - }, - shortcutsHandler: function(e, keys, origHandler) - { - // based on https://github.com/jeresig/jquery.hotkeys - var hotkeysSpecialKeys = - { - 8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", - 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=", - 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", - 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=", - 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'" - }; + var chars = { + '\u2122': '&trade;', + '\u00a9': '&copy;', + '\u2026': '&hellip;', + '\u2014': '&mdash;', + '\u2010': '&dash;' + }; + // replace special characters + $.each(chars, function(i,s) + { + html = html.replace(new RegExp(i, 'g'), s); + }); + // remove br in the of li + html = html.replace(new RegExp('<br\\s?/?></li>', 'gi'), '</li>'); + html = html.replace(new RegExp('</li><br\\s?/?>', 'gi'), '</li>'); + // remove verified + html = html.replace(new RegExp('<div(.*?[^>]) data-tagblock="redactor"(.*?[^>])>', 'gi'), '<div$1$2>'); + html = html.replace(new RegExp('<(.*?) 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(/ data-save-url="(.*?[^>])"/gi, ''); - var hotkeysShiftNums = - { - "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", - "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", - ".": ">", "/": "?", "\\": "|" - }; + // 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, ''); - keys = keys.toLowerCase().split(" "); - var special = hotkeysSpecialKeys[e.keyCode], - character = String.fromCharCode( e.which ).toLowerCase(), - modif = "", possible = {}; + // remove font tag + html = html.replace(/<font(.*?[^<])>/gi, ''); + html = html.replace(/<\/font>/gi, ''); - $.each([ "alt", "ctrl", "meta", "shift"], function(index, specialKey) - { - if (e[specialKey + 'Key'] && special !== specialKey) - { - modif += specialKey + '+'; - } - }); + // tidy html + html = this.tidy.load(html); + // link nofollow + if (this.opts.linkNofollow) + { + html = html.replace(/<a(.*?)rel="nofollow"(.*?[^>])>/gi, '<a$1$2>'); + html = html.replace(/<a(.*?[^>])>/gi, '<a$1 rel="nofollow">'); + } - if (special) - { - possible[modif + special] = true; - } + // 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(new RegExp('<(.*?) data-verified="redactor"(.*?[^>])>', 'gi'), '<$1$2>'); + html = html.replace(new RegExp('<(.*?) data-verified="redactor">', 'gi'), '<$1>'); - if (character) - { - possible[modif + character] = true; - possible[modif + hotkeysShiftNums[character]] = true; - - // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" - if (modif === "shift+") + return html; + }, + onPaste: function(html, setMode) { - possible[hotkeysShiftNums[character]] = true; - } - } + html = $.trim(html); - for (var i = 0, l = keys.length; i < l; i++) - { - if (possible[keys[i]]) - { - e.preventDefault(); - return origHandler.apply(this, arguments); - } - } - }, + html = html.replace(/\$/g, '&#36;'); + html = html.replace(/”/g, '"'); + html = html.replace(/“/g, '"'); + html = html.replace(/‘/g, '\''); + html = html.replace(/’/g, '\''); - // FOCUS - focus: function() - { - if (!this.browser('opera')) - { - this.window.setTimeout($.proxy(this.focusSet, this, true), 1); - } - else - { - this.$editor.focus(); - } - }, - focusWithSaveScroll: function() - { - if (this.browser('msie')) - { - var top = this.document.documentElement.scrollTop; - } + // convert dirty spaces + 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, ' '); - this.$editor.focus(); + if (this.opts.pastePlainText) + { + return this.clean.getPlainText(html); + } - if (this.browser('msie')) - { - this.document.documentElement.scrollTop = top; - } - }, - focusEnd: function() - { - if (!this.browser('mozilla')) - { - this.focusSet(); - } - else - { - if (this.opts.linebreaks === false) - { - var last = this.$editor.children().last(); + if (!this.utils.isSelectAll() && typeof setMode == 'undefined') + { + if (this.utils.isCurrentOrParent(['FIGCAPTION', 'A'])) + { + return this.clean.getPlainText(html, false); + } - this.$editor.focus(); - this.selectionEnd(last); - } - else - { - this.focusSet(); - } - } - }, - focusSet: function(collapse, element) - { - this.$editor.focus(); + if (this.utils.isCurrentOrParent('PRE')) + { + return this.clean.getPreCode(html); + } - if (typeof element == 'undefined') - { - element = this.$editor[0]; - } + if (this.utils.isCurrentOrParent(['BLOCKQUOTE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'])) + { + html = this.clean.getOnlyImages(html); - var range = this.getRange(); - range.selectNodeContents(element); + if (!this.utils.browser('msie')) + { + var block = this.selection.getBlock(); + if (block && block.tagName == 'P') + { + html = html.replace(/<img(.*?)>/gi, '<p><img$1></p>'); + } + } - // collapse - controls the position of focus: the beginning (true), at the end (false). - range.collapse(collapse || false); + return html; + } - var sel = this.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - }, + if (this.utils.isCurrentOrParent(['TD'])) + { + html = this.clean.onPasteTidy(html, 'td'); - // TOGGLE - toggle: function(direct) - { - if (this.opts.visual) this.toggleCode(direct); - else this.toggleVisual(); - }, - toggleVisual: function() - { - var html = this.$source.hide().val(); - if (typeof this.modified !== 'undefined') - { - var modified = this.modified.replace(/\n/g, ''); + if (this.opts.linebreaks) html = this.clean.replaceParagraphsToBr(html); - var thtml = html.replace(/\n/g, ''); - thtml = this.cleanRemoveSpaces(thtml, false); + html = this.clean.replaceDivsToBr(html); - this.modified = this.cleanRemoveSpaces(modified, false) !== thtml; - } + return html; + } - if (this.modified) - { - // don't remove the iframe even if cleared all. - if (this.opts.fullpage && html === '') - { - this.setFullpageOnInit(html); - } - else - { - this.set(html); - if (this.opts.fullpage) - { - this.buildBindKeyboard(); + + if (this.utils.isCurrentOrParent(['LI'])) + { + return this.clean.onPasteTidy(html, 'li'); + } } - } - this.callback('change', false, html); - } - if (this.opts.iframe) this.$frame.show(); - else this.$editor.show(); + html = this.clean.isSingleLine(html, setMode); - if (this.opts.fullpage) this.$editor.attr('contenteditable', true ); + if (!this.clean.singleLine) + { + if (this.opts.linebreaks) html = this.clean.replaceParagraphsToBr(html); + if (this.opts.replaceDivs) html = this.clean.replaceDivs(html); - this.$source.off('keydown.redactor-textarea-indenting'); + html = this.clean.saveFormTags(html); + } - this.$editor.focus(); - this.selectionRestore(); - this.observeStart(); - this.buttonActiveVisual(); - this.buttonInactive('html'); - this.opts.visual = true; + html = this.clean.onPasteWord(html); + html = this.clean.onPasteExtra(html); + html = this.clean.onPasteTidy(html, 'all'); - }, - toggleCode: function(direct) - { - if (direct !== false) this.selectionSave(); - var height = null; - if (this.opts.iframe) - { - height = this.$frame.height(); - if (this.opts.fullpage) this.$editor.removeAttr('contenteditable'); - this.$frame.hide(); - } - else - { - height = this.$editor.innerHeight(); - this.$editor.hide(); - } + // paragraphize + if (!this.clean.singleLine && this.opts.paragraphize) + { + html = this.paragraphize.load(html); + } - var html = this.$source.val(); + html = this.clean.removeDirtyStyles(html); + html = this.clean.onPasteRemoveSpans(html); + html = this.clean.onPasteRemoveEmpty(html); - // tidy html - if (html !== '' && this.opts.tidyHtml) - { - this.$source.val(this.cleanHtml(html)); - } + html = this.clean.convertInline(html); - this.modified = html; + return html; + }, + onPasteWord: function(html) + { + // comments + html = html.replace(/<!--[\s\S]*?-->/gi, ''); - this.$source.height(height).show().focus(); + // style + html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ''); - // textarea indenting - this.$source.on('keydown.redactor-textarea-indenting', this.textareaIndenting); + if (/(class=\"?Mso|style=\"[^\"]*\bmso\-|w:WordDocument)/.test(html)) + { + html = this.clean.onPasteIeFixLinks(html); - this.buttonInactiveVisual(); - this.buttonActive('html'); - this.opts.visual = false; - }, - textareaIndenting: function(e) - { - if (e.keyCode === 9) - { - var $el = $(this); - 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; - } - }, + // shapes + html = html.replace(/<img(.*?)v:shapes=(.*?)>/gi, ''); + html = html.replace(/src="file\:\/\/(.*?)"/, 'src=""'); - // AUTOSAVE - autosave: function() - { - var savedHtml = false; - this.autosaveInterval = setInterval($.proxy(function() - { - var html = this.get(); - if (savedHtml !== html) - { - var name = this.$source.attr('name'); - $.ajax({ - url: this.opts.autosave, - type: 'post', - data: 'name=' + name + '&' + name + '=' + escape(encodeURIComponent(html)), - success: $.proxy(function(data) - { - var json = $.parseJSON(data); - if (typeof json.error == 'undefined') - { - // success - this.callback('autosave', false, json); - } - else - { - // error - this.callback('autosaveError', false, json); - } + // list + html = html.replace(/<p(.*?)class="MsoListParagraphCxSpFirst"([\w\W]*?)<\/p>/gi, '<ul><li$2</li>'); + html = html.replace(/<p(.*?)class="MsoListParagraphCxSpMiddle"([\w\W]*?)<\/p>/gi, '<li$2</li>'); + html = html.replace(/<p(.*?)class="MsoListParagraphCxSpLast"([\w\W]*?)<\/p>/gi, '<li$2</li></ul>'); + // one line + html = html.replace(/<p(.*?)class="MsoListParagraph"([\w\W]*?)<\/p>/gi, '<ul><li$2</li></ul>'); + // remove ms word's bullet + html = html.replace(/·/g, ''); + html = html.replace(/<p class="Mso(.*?)"/gi, '<p'); - savedHtml = html; + // classes + html = html.replace(/ class=\"(mso[^\"]*)\"/gi, ""); + html = html.replace(/ class=(mso\w+)/gi, ""); - }, this) - }); - } - }, this), this.opts.autosaveInterval*1000); - }, + // remove ms word tags + html = html.replace(/<o:p(.*?)>([\w\W]*?)<\/o:p>/gi, '$2'); - // TOOLBAR - toolbarBuild: function() - { - // hide on mobile - if (this.isMobile() && this.opts.buttonsHideOnMobile.length > 0) - { - $.each(this.opts.buttonsHideOnMobile, $.proxy(function(i, s) - { - var index = this.opts.buttons.indexOf(s); - this.opts.buttons.splice(index, 1); + // ms word break lines + html = html.replace(/\n/g, ' '); - }, this)); - } + // ms word lists break lines + html = html.replace(/<p>\n?<li>/gi, '<li>'); + } - // extend buttons - if (this.opts.air) - { - this.opts.buttons = this.opts.airButtons; - } - else - { - if (!this.opts.buttonSource) - { - var index = this.opts.buttons.indexOf('html'); - this.opts.buttons.splice(index, 1); - } - } + // remove nbsp + if (this.opts.cleanSpaces) + { + html = html.replace(/(\s|&nbsp;)+/g, ' '); + } - // formatting tags - if (this.opts.toolbar) - { - $.each(this.opts.toolbar.formatting.dropdown, $.proxy(function (i, s) + return html; + }, + onPasteExtra: function(html) { - if ($.inArray(i, this.opts.formattingTags ) == '-1') delete this.opts.toolbar.formatting.dropdown[i]; + // remove google docs markers + html = html.replace(/<b\sid="internal-source-marker(.*?)">([\w\W]*?)<\/b>/gi, "$2"); + html = html.replace(/<b(.*?)id="docs-internal-guid(.*?)">([\w\W]*?)<\/b>/gi, "$3"); - }, this)); - } + // google docs styles + html = html.replace(/<span[^>]*(font-style: italic; font-weight: bold|font-weight: bold; font-style: italic)[^>]*>/gi, '<span style="font-weight: bold;"><span style="font-style: italic;">'); + html = html.replace(/<span[^>]*font-style: italic[^>]*>/gi, '<span style="font-style: italic;">'); + html = html.replace(/<span[^>]*font-weight: bold[^>]*>/gi, '<span style="font-weight: bold;">'); + html = html.replace(/<span[^>]*text-decoration: underline[^>]*>/gi, '<span style="text-decoration: underline;">'); - // if no buttons don't create a toolbar - if (this.opts.buttons.length === 0) return false; + html = html.replace(/<img>/gi, ''); + html = html.replace(/\n{3,}/gi, '\n'); + html = html.replace(/<font(.*?)>([\w\W]*?)<\/font>/gi, '$2'); - // air enable - this.airEnable(); + // remove dirty p + html = html.replace(/<p><p>/gi, '<p>'); + html = html.replace(/<\/p><\/p>/gi, '</p>'); + html = html.replace(/<li>(\s*|\t*|\n*)<p>/gi, '<li>'); + html = html.replace(/<\/p>(\s*|\t*|\n*)<\/li>/gi, '</li>'); - // toolbar build - this.$toolbar = $('<ul>').addClass('redactor_toolbar').attr('id', 'redactor_toolbar_' + this.uuid); + // remove space between paragraphs + html = html.replace(/<\/p>\s<p/gi, '<\/p><p'); - if (this.opts.typewriter) - { - this.$toolbar.addClass('redactor-toolbar-typewriter'); - } + // remove safari local images + html = html.replace(/<img src="webkit-fake-url\:\/\/(.*?)"(.*?)>/gi, ''); - if (this.opts.toolbarOverflow && this.isMobile()) - { - this.$toolbar.addClass('redactor-toolbar-overflow'); - } + // bullets + html = html.replace(/<p>•([\w\W]*?)<\/p>/gi, '<li>$1</li>'); - if (this.opts.air) - { - // air box - this.$air = $('<div class="redactor_air">').attr('id', 'redactor_air_' + this.uuid).hide(); - this.$air.append(this.$toolbar); - $('body').append(this.$air); - } - else - { - if (this.opts.toolbarExternal) - { - this.$toolbar.addClass('redactor-toolbar-external'); - $(this.opts.toolbarExternal).html(this.$toolbar); - } - else this.$box.prepend(this.$toolbar); - } + // FF fix + if (this.utils.browser('mozilla')) + { + html = html.replace(/<br\s?\/?>$/gi, ''); + } - $.each(this.opts.buttons, $.proxy(function(i, btnName) - { - if (this.opts.toolbar[btnName]) + return html; + }, + onPasteTidy: function(html, type) { - var btnObject = this.opts.toolbar[btnName]; - if (this.opts.fileUpload === false && btnName === 'file') return true; - this.$toolbar.append( $('<li>').append(this.buttonBuild(btnName, btnObject))); - } + // remove all tags except these + var tags = ['span', 'a', 'pre', 'blockquote', 'small', 'em', 'strong', 'code', 'kbd', 'mark', 'address', 'cite', 'var', 'samp', 'dfn', 'sup', 'sub', 'b', 'i', 'u', 'del', + 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'p', 'br', 'video', 'audio', 'iframe', 'embed', 'param', 'object', 'img', 'table', + 'td', 'th', 'tr', 'tbody', 'tfoot', 'thead', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + var tagsEmpty = false; + var attrAllowed = [ + ['a', '*'], + ['img', ['src', 'alt']], + ['span', ['class', 'rel', 'data-verified']], + ['iframe', '*'], + ['video', '*'], + ['audio', '*'], + ['embed', '*'], + ['object', '*'], + ['param', '*'], + ['source', '*'] + ]; - }, this)); + if (type == 'all') + { + tagsEmpty = ['p', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + attrAllowed = [ + ['table', 'class'], + ['td', ['colspan', 'rowspan']], + ['a', '*'], + ['img', ['src', 'alt', 'data-redactor-inserted-image']], + ['span', ['class', 'rel', 'data-verified']], + ['iframe', '*'], + ['video', '*'], + ['audio', '*'], + ['embed', '*'], + ['object', '*'], + ['param', '*'], + ['source', '*'] + ]; + } + else if (type == 'td') + { + // remove all tags except these and remove all table tags: tr, td etc + tags = ['ul', 'ol', 'li', 'span', 'a', 'small', 'em', 'strong', 'code', 'kbd', 'mark', 'cite', 'var', 'samp', 'dfn', 'sup', 'sub', 'b', 'i', 'u', 'del', + 'ol', 'ul', 'li', 'dl', 'dt', 'dd', 'br', 'iframe', 'video', 'audio', 'embed', 'param', 'object', 'img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - this.$toolbar.find('a').attr('tabindex', '-1'); + } + else if (type == 'li') + { + // only inline tags and ul, ol, li + tags = ['ul', 'ol', 'li', 'span', 'a', 'small', 'em', 'strong', 'code', 'kbd', 'mark', 'cite', 'var', 'samp', 'dfn', 'sup', 'sub', 'b', 'i', 'u', 'del', 'br', + 'iframe', 'video', 'audio', 'embed', 'param', 'object', 'img']; + } - // fixed - if (this.opts.toolbarFixed) - { - this.toolbarObserveScroll(); - $(this.opts.toolbarFixedTarget).on('scroll.redactor', $.proxy(this.toolbarObserveScroll, this)); - } + var options = { + deniedTags: false, + allowedTags: tags, + removeComments: true, + removePhp: true, + removeAttr: false, + allowedAttr: attrAllowed, + removeEmpty: tagsEmpty + }; - // buttons response - if (this.opts.activeButtons) - { - this.$editor.on('mouseup.redactor keyup.redactor', $.proxy(this.buttonActiveObserver, this)); - } - }, - toolbarObserveScroll: function() - { - var scrollTop = $(this.opts.toolbarFixedTarget).scrollTop(); - var boxTop = 0; - var left = 0; - var end = 0; + return this.tidy.load(html, options); - if (this.opts.toolbarFixedTarget === document) - { - boxTop = this.$box.offset().top; - } - else - { - boxTop = 1; - } + }, + onPasteRemoveEmpty: function(html) + { + html = html.replace(/<(p|h[1-6])>(|\s|\n|\t|<br\s?\/?>)<\/(p|h[1-6])>/gi, ''); - end = boxTop + this.$box.height() + 40; + // remove br in the end + if (!this.opts.linebreaks) html = html.replace(/<br>$/i, ''); - if (scrollTop > boxTop) - { - var width = '100%'; - if (this.opts.toolbarFixedBox) + return html; + }, + onPasteRemoveSpans: function(html) { - left = this.$box.offset().left; - width = this.$box.innerWidth(); - this.$toolbar.addClass('toolbar_fixed_box'); - } + html = html.replace(/<span>(.*?)<\/span>/gi, '$1'); + html = html.replace(/<span[^>]*>\s|&nbsp;<\/span>/gi, ' '); - this.toolbarFixed = true; + return html; + }, + onPasteIeFixLinks: function(html) + { + if (!this.utils.browser('msie')) return html; - if (this.opts.toolbarFixedTarget === document) + var tmp = $.trim(html); + if (tmp.search(/^<a(.*?)>(.*?)<\/a>$/i) === 0) + { + html = html.replace(/^<a(.*?)>(.*?)<\/a>$/i, "$2"); + } + + return html; + }, + isSingleLine: function(html, setMode) { - this.$toolbar.css({ - position: 'fixed', - width: width, - zIndex: 10005, - top: this.opts.toolbarFixedTopOffset + 'px', - left: left - }); - } - else - { - this.$toolbar.css({ - position: 'absolute', - width: width, - zIndex: 10005, - top: (this.opts.toolbarFixedTopOffset + scrollTop) + 'px', - left: 0 - }); - } + this.clean.singleLine = false; - if (scrollTop < end) this.$toolbar.css('visibility', 'visible'); - else this.$toolbar.css('visibility', 'hidden'); - } - else - { - this.toolbarFixed = false; - this.$toolbar.css({ - position: 'relative', - width: 'auto', - top: 0, - left: left - }); + if (!this.utils.isSelectAll() && typeof setMode == 'undefined') + { + var blocks = this.opts.blockLevelElements.join('|').replace('P|', '').replace('DIV|', ''); - if (this.opts.toolbarFixedBox) this.$toolbar.removeClass('toolbar_fixed_box'); - } - }, + var matchBlocks = html.match(new RegExp('</(' + blocks + ')>', 'gi')); + var matchContainers = html.match(/<\/(p|div)>/gi); - // AIR - airEnable: function() - { - if (!this.opts.air) return; + if (!matchBlocks && (matchContainers === null || (matchContainers && matchContainers.length <= 1))) + { + var matchBR = html.match(/<br\s?\/?>/gi); + var matchIMG = html.match(/<img(.*?[^>])>/gi); + if (!matchBR && !matchIMG) + { + this.clean.singleLine = true; + html = html.replace(/<\/?(p|div)(.*?)>/gi, ''); + } + } + } - this.$editor.on('mouseup.redactor keyup.redactor', this, $.proxy(function(e) - { - var text = this.getSelectionText(); + return html; + }, + stripTags: function(input, allowed) + { + allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join(''); + var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi; - if (e.type === 'mouseup' && text != '') this.airShow(e); - if (e.type === 'keyup' && e.shiftKey && text != '') + return input.replace(tags, function ($0, $1) { + return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; + }); + }, + savePreCode: function(html) { - var $focusElem = $(this.getElement(this.getSelection().focusNode)), offset = $focusElem.offset(); - offset.height = $focusElem.height(); - this.airShow(offset, true); - } + var pre = html.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/gi); + if (pre !== null) + { + $.each(pre, $.proxy(function(i,s) + { + var arr = s.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/i); - }, this)); - }, - airShow: function (e, keyboard) - { - if (!this.opts.air) return; + arr[3] = arr[3].replace(/<br\s?\/?>/g, '\n'); + arr[3] = arr[3].replace(/&nbsp;/g, ' '); - this.selectionSave(); + if (this.opts.preSpaces) + { + arr[3] = arr[3].replace(/\t/g, Array(this.opts.preSpaces + 1).join(' ')); + } - var left, top; - $('.redactor_air').hide(); + arr[3] = this.clean.encodeEntities(arr[3]); - if (keyboard) - { - left = e.left; - top = e.top + e.height + 14; + // $ fix + arr[3] = arr[3].replace(/\$/g, '&#36;'); - if (this.opts.iframe) - { - top += this.$box.position().top - $(this.document).scrollTop(); - left += this.$box.position().left; - } - } - else - { - var width = this.$air.innerWidth(); + html = html.replace(s, '<' + arr[1] + arr[2] + '>' + arr[3] + '</' + arr[1] + '>'); - left = e.clientX; - if ($(this.document).width() < (left + width)) left -= width; + }, this)); + } - top = e.clientY + 14; - if (this.opts.iframe) + return html; + }, + getTextFromHtml: function(html) { - top += this.$box.position().top; - left += this.$box.position().left; - } - else top += $( this.document ).scrollTop(); - } + html = html.replace(/<br\s?\/?>|<\/H[1-6]>|<\/p>|<\/div>|<\/li>|<\/td>/gi, '\n'); - this.$air.css({ - left: left + 'px', - top: top + 'px' - }).show(); + var tmp = document.createElement('div'); + tmp.innerHTML = html; + html = tmp.textContent || tmp.innerText; - this.airBindHide(); - }, - airBindHide: function() - { - if (!this.opts.air) return; - - var hideHandler = $.proxy(function(doc) - { - $(doc).on('mousedown.redactor', $.proxy(function(e) + return $.trim(html); + }, + getPlainText: function(html, paragraphize) { - if ($( e.target ).closest(this.$toolbar).length === 0) + html = this.clean.getTextFromHtml(html); + html = html.replace(/\n/g, '<br />'); + + if (this.opts.paragraphize && typeof paragraphize == 'undefined') { - this.$air.fadeOut(100); - this.selectionRemove(); - $(doc).off(e); + html = this.paragraphize.load(html); } - }, this)).on('keydown.redactor', $.proxy(function(e) + return html; + }, + getPreCode: function(html) { - if (e.which === this.keyCode.ESC) + html = html.replace(/<img(.*?) style="(.*?)"(.*?[^>])>/gi, '<img$1$3>'); + html = html.replace(/<img(.*?)>/gi, '&lt;img$1&gt;'); + html = this.clean.getTextFromHtml(html); + + if (this.opts.preSpaces) { - this.getSelection().collapseToStart(); + html = html.replace(/\t/g, Array(this.opts.preSpaces + 1).join(' ')); } - this.$air.fadeOut(100); - $(doc).off(e); + html = this.clean.encodeEntities(html); - }, this)); - }, this); - - // Hide the toolbar at events in all documents (iframe) - hideHandler(document); - if (this.opts.iframe) hideHandler(this.document); - }, - airBindMousemoveHide: function() - { - if (!this.opts.air) return; - - var hideHandler = $.proxy(function(doc) - { - $(doc).on('mousemove.redactor', $.proxy(function(e) + return html; + }, + getOnlyImages: function(html) { - if ($( e.target ).closest(this.$toolbar).length === 0) - { - this.$air.fadeOut(100); - $(doc).off(e); - } + html = html.replace(/<img(.*?)>/gi, '[img$1]'); - }, this)); - }, this); + // remove all tags + html = html.replace(/<([Ss]*?)>/gi, ''); - // Hide the toolbar at events in all documents (iframe) - hideHandler(document); - if (this.opts.iframe) hideHandler(this.document); - }, + html = html.replace(/\[img(.*?)\]/gi, '<img$1>'); - // DROPDOWNS - dropdownBuild: function($dropdown, dropdownObject) - { - $.each(dropdownObject, $.proxy(function(btnName, btnObject) - { - if (!btnObject.className) btnObject.className = ''; - - var $item; - if (btnObject.name === 'separator') $item = $('<a class="redactor_separator_drop">'); - else + return html; + }, + getOnlyLinksAndImages: function(html) { - $item = $('<a href="#" class="' + btnObject.className + ' redactor_dropdown_' + btnName + '">' + btnObject.title + '</a>'); - $item.on('click', $.proxy(function(e) - { - if (this.opts.air) - { - this.selectionRestore(); - } + html = html.replace(/<a(.*?)href="(.*?)"(.*?)>([\w\W]*?)<\/a>/gi, '[a href="$2"]$4[/a]'); + html = html.replace(/<img(.*?)>/gi, '[img$1]'); - if (e.preventDefault) e.preventDefault(); - if (this.browser('msie')) e.returnValue = false; + // remove all tags + html = html.replace(/<(.*?)>/gi, ''); - if (btnObject.callback) btnObject.callback.call(this, btnName, $item, btnObject, e); - if (btnObject.exec) this.execCommand(btnObject.exec, btnName); - if (btnObject.func) this[btnObject.func](btnName); + html = html.replace(/\[a href="(.*?)"\]([\w\W]*?)\[\/a\]/gi, '<a href="$1">$2</a>'); + html = html.replace(/\[img(.*?)\]/gi, '<img$1>'); - this.buttonActiveObserver(); - if (this.opts.air) this.$air.fadeOut(100); + return html; + }, + encodeEntities: function(str) + { + str = String(str).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"'); + return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); + }, + removeDirtyStyles: function(html) + { + if (this.utils.browser('msie')) return html; + var div = document.createElement('div'); + div.innerHTML = html; - }, this)); - } + this.clean.clearUnverifiedRemove($(div)); - $dropdown.append($item); + html = div.innerHTML; + $(div).remove(); - }, this)); - }, - dropdownShow: function(e, key) - { - if (!this.opts.visual) - { - e.preventDefault(); - return false; - } + return html; + }, + clearUnverified: function() + { + if (this.utils.browser('msie')) return; - var $button = this.buttonGet(key); + this.clean.clearUnverifiedRemove(this.$editor); - // 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); + var headers = this.$editor.find('h1, h2, h3, h4, h5, h6'); + headers.find('span').removeAttr('style'); + headers.find(this.opts.verifiedTags.join(', ')).removeAttr('style'); - if ($button.hasClass('dropact')) this.dropdownHideAll(); - else - { - this.dropdownHideAll(); - this.callback('dropdownShow', { dropdown: $dropdown, key: key, button: $button }); + this.code.sync(); + }, + clearUnverifiedRemove: function($editor) + { + $editor.find(this.opts.verifiedTags.join(', ')).removeAttr('style'); + $editor.find('span').not('[data-verified="redactor"]').removeAttr('style'); - this.buttonActive(key); - $button.addClass('dropact'); + $editor.find('span[data-verified="redactor"], img[data-verified="redactor"]').each(function(i, s) + { + var $s = $(s); + $s.attr('style', $s.attr('rel')); + }); - var keyPosition = $button.offset(); - - // fix right placement - var dropdownWidth = $dropdown.width(); - if ((keyPosition.left + dropdownWidth) > $(document).width()) + }, + setVerified: function(html) { - keyPosition.left -= dropdownWidth; - } + if (this.utils.browser('msie')) return html; - var left = keyPosition.left + 'px'; - var btnHeight = $button.innerHeight(); + html = html.replace(new RegExp('<img(.*?[^>])>', 'gi'), '<img$1 data-verified="redactor">'); + html = html.replace(new RegExp('<span(.*?)>', 'gi'), '<span$1 data-verified="redactor">'); - var position = 'absolute'; - var top = (btnHeight + this.opts.toolbarFixedTopOffset) + 'px'; + 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); + } + catch (e) {} + } + } - if (this.opts.toolbarFixed && this.toolbarFixed) position = 'fixed'; - else top = keyPosition.top + btnHeight + 'px'; + return html; + }, + convertInline: function(html) + { + var $div = $('<div />').html(html); - $dropdown.css({ position: position, left: left, top: top }).show(); - this.callback('dropdownShown', { dropdown: $dropdown, key: key, button: $button }); - } + var tags = this.opts.inlineTags; + tags.push('span'); + $div.find(tags.join(',')).each(function() + { + var $el = $(this); + var tag = this.tagName.toLowerCase(); + $el.attr('data-redactor-tag', tag); - var hdlHideDropDown = $.proxy(function(e) - { - this.dropdownHide(e, $dropdown); + if (tag == 'span') + { + if ($el.attr('style')) $el.attr('data-redactor-style', $el.attr('style')); + else if ($el.attr('class')) $el.attr('data-redactor-class', $el.attr('class')); + } - }, this); + }); - $(document).one('click', hdlHideDropDown); - this.$editor.one('click', hdlHideDropDown); - this.$editor.one('touchstart', hdlHideDropDown); + html = $div.html(); + $div.remove(); + return html; + }, + normalizeLists: function() + { + this.$editor.find('li').each(function(i,s) + { + var $next = $(s).next(); + if ($next.length !== 0 && ($next[0].tagName == 'UL' || $next[0].tagName == 'OL')) + { + $(s).append($next); + } - e.stopPropagation(); - this.focusWithSaveScroll(); - }, - dropdownHideAll: function() - { - this.$toolbar.find('a.dropact').removeClass('redactor_act').removeClass('dropact'); - $('.redactor_dropdown').hide(); - this.callback('dropdownHide'); - }, - dropdownHide: function (e, $dropdown) - { - if (!$(e.target).hasClass('dropact')) - { - $dropdown.removeClass('dropact'); - this.dropdownHideAll(); - } - }, - - // BUTTONS - buttonBuild: function(btnName, btnObject, buttonImage) - { - var $button = $('<a href="javascript:;" title="' + btnObject.title + '" tabindex="-1" class="re-icon re-' + btnName + '"></a>'); - - if (typeof buttonImage != 'undefined') - { - $button.addClass('redactor-btn-image'); - } - - $button.on('click', $.proxy(function(e) - { - if (e.preventDefault) e.preventDefault(); - if (this.browser('msie')) e.returnValue = false; - - if ($button.hasClass('redactor_button_disabled')) return false; - - if (this.isFocused() === false && !btnObject.exec) + }); + }, + removeSpaces: function(html) { - this.focusWithSaveScroll(); - } + html = html.replace(/\n/g, ''); + html = html.replace(/[\t]*/g, ''); + 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, ''); - if (btnObject.exec) + return html; + }, + replaceDivs: function(html) { - this.focusWithSaveScroll(); + if (this.opts.linebreaks) + { + html = html.replace(/<div><br\s?\/?><\/div>/gi, '<br />'); + html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '$2<br />'); + } + else + { + html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '<p$1>$2</p>'); + } - this.execCommand(btnObject.exec, btnName); - this.airBindMousemoveHide(); - - } - else if (btnObject.func && btnObject.func !== 'show') + return html; + }, + replaceDivsToBr: function(html) { - this[btnObject.func](btnName); - this.airBindMousemoveHide(); + html = html.replace(/<div\s(.*?)>/gi, '<p>'); + html = html.replace(/<div><br\s?\/?><\/div>/gi, '<br /><br />'); + html = html.replace(/<div>([\w\W]*?)<\/div>/gi, '$1<br /><br />'); - } - else if (btnObject.callback) + return html; + }, + replaceParagraphsToBr: function(html) { - btnObject.callback.call(this, btnName, $button, btnObject, e); - this.airBindMousemoveHide(); + html = html.replace(/<p\s(.*?)>/gi, '<p>'); + html = html.replace(/<p><br\s?\/?><\/p>/gi, '<br />'); + html = html.replace(/<p>([\w\W]*?)<\/p>/gi, '$1<br /><br />'); + html = html.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>'); - } - else if (btnObject.dropdown) + return html; + }, + saveFormTags: function(html) { - this.dropdownShow(e, btnName); + return html.replace(/<form(.*?)>([\w\W]*?)<\/form>/gi, '<section$1 rel="redactor-form-tag">$2</section>'); + }, + restoreFormTags: function(html) + { + return html.replace(/<section(.*?) rel="redactor-form-tag"(.*?)>([\w\W]*?)<\/section>/gi, '<form$1$2>$3</form>'); } - - this.buttonActiveObserver(false, btnName); - - }, this)); - - // dropdown - if (btnObject.dropdown) - { - var $dropdown = $('<div class="redactor_dropdown redactor_dropdown_box_' + btnName + '" style="display: none;">'); - $button.data('dropdown', $dropdown); - this.dropdownBuild($dropdown, btnObject.dropdown); - } - - return $button; + }; }, - buttonGet: function(key) + code: function() { - if (!this.opts.toolbar) return false; - return $(this.$toolbar.find('a.re-' + key)); - }, - buttonTagToActiveState: function(buttonName, tagName) - { - this.opts.activeButtons.push(buttonName); - this.opts.activeButtonsStates[tagName] = buttonName; - }, - buttonActiveToggle: function(key) - { - var btn = this.buttonGet(key); + return { + set: function(html) + { + html = $.trim(html.toString()); - if (btn.hasClass('redactor_act')) - { - this.buttonInactive(key); - } - else - { - this.buttonActive(key); - } - }, - buttonActive: function(key) - { - var btn = this.buttonGet(key); - btn.addClass('redactor_act'); - }, - buttonInactive: function(key) - { - var btn = this.buttonGet(key); - btn.removeClass('redactor_act'); - }, - buttonInactiveAll: function(btnName) - { - this.$toolbar.find('a.re-icon').not('.re-' + btnName).removeClass('redactor_act'); - }, - buttonActiveVisual: function() - { - this.$toolbar.find('a.re-icon').not('a.re-html').removeClass('redactor_button_disabled'); - }, - buttonInactiveVisual: function() - { - this.$toolbar.find('a.re-icon').not('a.re-html').addClass('redactor_button_disabled'); - }, - buttonChangeIcon: function (key, classname) - { - this.buttonGet(key).addClass('re-' + classname); - }, - buttonRemoveIcon: function(key, classname) - { - this.buttonGet(key).removeClass('re-' + classname); - }, - buttonAwesome: function(key, name) - { - var button = this.buttonGet(key); - button.removeClass('redactor-btn-image'); - button.addClass('fa-redactor-btn'); - button.html('<i class="fa ' + name + '"></i>'); - }, - buttonAdd: function(key, title, callback, dropdown) - { - if (!this.opts.toolbar) return; - var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true); + // clean + html = this.clean.onSet(html); - this.$toolbar.append($('<li>').append(btn)); + this.$editor.html(html); + this.code.sync(); - return btn; - }, - buttonAddFirst: function(key, title, callback, dropdown) - { - if (!this.opts.toolbar) return; - var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true); - this.$toolbar.prepend($('<li>').append(btn)); - }, - buttonAddAfter: function(afterkey, key, title, callback, dropdown) - { - if (!this.opts.toolbar) return; - var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true); - var $btn = this.buttonGet(afterkey); + setTimeout($.proxy(this.buffer.add, this), 15); + if (this.start === false) this.observe.load(); - if ($btn.size() !== 0) $btn.parent().after($('<li>').append(btn)); - else this.$toolbar.append($('<li>').append(btn)); - - return btn; - }, - buttonAddBefore: function(beforekey, key, title, callback, dropdown) - { - if (!this.opts.toolbar) return; - var btn = this.buttonBuild(key, { title: title, callback: callback, dropdown: dropdown }, true); - var $btn = this.buttonGet(beforekey); - - if ($btn.size() !== 0) $btn.parent().before($('<li>').append(btn)); - else this.$toolbar.append($('<li>').append(btn)); - - return btn; - }, - buttonRemove: function (key) - { - var $btn = this.buttonGet(key); - $btn.remove(); - }, - buttonActiveObserver: function(e, btnName) - { - var parent = this.getParent(); - this.buttonInactiveAll(btnName); - - if (e === false && btnName !== 'html') - { - if ($.inArray(btnName, this.opts.activeButtons) != -1) + }, + get: function() { - this.buttonActiveToggle(btnName); - } - return; - } + var code = this.$textarea.val(); - if (parent && parent.tagName === 'A') this.$toolbar.find('a.redactor_dropdown_link').text(this.opts.curLang.link_edit); - else this.$toolbar.find('a.redactor_dropdown_link').text(this.opts.curLang.link_insert); + // indent code + code = this.tabifier.get(code); - $.each(this.opts.activeButtonsStates, $.proxy(function(key, value) - { - if ($(parent).closest(key, this.$editor.get()[0]).length != 0) + return code; + }, + sync: function() { - this.buttonActive(value); - } + setTimeout($.proxy(this.code.startSync, this), 10); + }, + startSync: function() + { + var html = this.$editor.html(); - }, this)); + // is there a need to synchronize + if (this.code.syncCode && this.code.syncCode == html) + { + // do not sync + return; + } - var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]); - if ($parent.length) - { - var align = $parent.css('text-align'); - if (align == '') - { - align = 'left'; - } + // save code + this.code.syncCode = html; - this.buttonActive('align' + align); - } - }, + // before clean callback + html = this.core.setCallback('syncBefore', html); - // EXEC - execPasteFrag: function(html) - { - var sel = this.getSelection(); - if (sel.getRangeAt && sel.rangeCount) - { - var range = this.getRange(); - range.deleteContents(); + // clean + html = this.clean.onSync(html); - var el = this.document.createElement("div"); - el.innerHTML = html; + // set code + this.$textarea.val(html); - var frag = this.document.createDocumentFragment(), node, lastNode; - while ((node = el.firstChild)) - { - lastNode = frag.appendChild(node); - } + // after sync callback + this.core.setCallback('sync', html); - var firstNode = frag.firstChild; - range.insertNode(frag); + if (this.start === false) + { + this.core.setCallback('change', html); + } - if (lastNode) - { - range = range.cloneRange(); - range.setStartAfter(lastNode); - range.collapse(true); - } - sel.removeAllRanges(); - sel.addRange(range); - } - }, - exec: function(cmd, param, sync) - { - if (cmd === 'formatblock' && this.browser('msie')) - { - param = '<' + param + '>'; - } + this.start = false; - if (cmd === 'inserthtml' && this.browser('msie')) - { - if (!this.isIe11()) + // autosave on change + this.autosave.onChange(); + }, + toggle: function() { - this.focusWithSaveScroll(); - this.document.selection.createRange().pasteHTML(param); - } - else this.execPasteFrag(param); - } - else - { - this.document.execCommand(cmd, false, param); - } + if (this.opts.visual) + { + this.code.showCode(); + } + else + { + this.code.showVisual(); + } + }, + showCode: function() + { + this.code.offset = this.caret.getOffset(); + var scroll = $(window).scrollTop(); - if (sync !== false) this.sync(); - this.callback('execCommand', cmd, param); - }, - execCommand: function(cmd, param, sync) - { - if (!this.opts.visual) - { - this.$source.focus(); - return false; - } + var height = this.$editor.innerHeight(); - if ( cmd === 'bold' - || cmd === 'italic' - || cmd === 'underline' - || cmd === 'strikethrough') - { - this.bufferSet(); - } + this.$editor.hide(); + var html = this.$textarea.val(); + this.modified = this.clean.removeSpaces(html); - if (cmd === 'superscript' || cmd === 'subscript') - { - var parent = this.getParent(); - if (parent.tagName === 'SUP' || parent.tagName === 'SUB') - { - this.inlineRemoveFormatReplace(parent); - } - } + // indent code + html = this.tabifier.get(html); - if (cmd === 'inserthtml') - { - this.insertHtml(param, sync); - this.callback('execCommand', cmd, param); - return; - } + this.$textarea.val(html).height(height).show().focus(); + this.$textarea.on('keydown.redactor-textarea-indenting', this.code.textareaIndenting); - // Stop formatting pre - if (this.currentOrParentIs('PRE') && !this.opts.formattingPre) return false; + $(window).scrollTop(scroll); - // Lists - if (cmd === 'insertunorderedlist' || cmd === 'insertorderedlist') return this.execLists(cmd, param); + if (this.$textarea[0].setSelectionRange) + { + this.$textarea[0].setSelectionRange(0, 0); + } - // Unlink - if (cmd === 'unlink') return this.execUnlink(cmd, param); + this.$textarea[0].scrollTop = 0; - // Usual exec - this.exec(cmd, param, sync); + this.opts.visual = false; - // Line - if (cmd === 'inserthorizontalrule') this.$editor.find('hr').removeAttr('id'); + this.button.setInactiveInCode(); + this.button.setActive('html'); + this.core.setCallback('source', html); + }, + showVisual: function() + { + if (this.opts.visual) return; - }, - execUnlink: function(cmd, param) - { - this.bufferSet(); + var html = this.$textarea.hide().val(); - var link = this.currentOrParentIs('A'); - if (link) - { - $(link).replaceWith($(link).text()); + if (this.modified !== this.clean.removeSpaces(html)) + { + this.code.set(html); + } - this.sync(); - this.callback('execCommand', cmd, param); - return; - } - }, - execLists: function(cmd, param) - { - this.bufferSet(); + this.$editor.show(); - var parent = this.getParent(); - var $list = $(parent).closest('ol, ul'); + if (!this.utils.isEmpty(html)) + { + this.placeholder.remove(); + } - if (!this.isParentRedactor($list) && $list.size() != 0) - { - $list = false; - } + this.caret.setOffset(this.code.offset); - var remove = false; + this.$textarea.off('keydown.redactor-textarea-indenting'); - if ($list && $list.length) - { - remove = true; - var listTag = $list[0].tagName; - if ((cmd === 'insertunorderedlist' && listTag === 'OL') - || (cmd === 'insertorderedlist' && listTag === 'UL')) - { - remove = false; - } - } + this.button.setActiveInVisual(); + this.button.setInactive('html'); - this.selectionSave(); + this.observe.load(); + this.opts.visual = true; + }, + textareaIndenting: function(e) + { + if (e.keyCode !== 9) return true; - // remove lists - if (remove) - { + var $el = this.$textarea; + var start = $el.get(0).selectionStart; + $el.val($el.val().substring(0, start) + "\t" + $el.val().substring($el.get(0).selectionEnd)); + $el.get(0).selectionStart = $el.get(0).selectionEnd = start + 1; - var nodes = this.getNodes(); - var elems = this.getBlocks(nodes); - - if (typeof nodes[0] != 'undefined' && nodes.length > 1 && nodes[0].nodeType == 3) - { - // fix the adding the first li to the array - elems.unshift(this.getBlock()); + return false; } - - var data = '', replaced = ''; - $.each(elems, $.proxy(function(i,s) + }; + }, + core: function() + { + return { + getObject: function() { - if (s.tagName == 'LI') + return $.extend({}, this); + }, + getEditor: function() + { + return this.$editor; + }, + getBox: function() + { + return this.$box; + }, + getElement: function() + { + return this.$element; + }, + getTextarea: function() + { + return this.$textarea; + }, + getToolbar: function() + { + return (this.$toolbar) ? this.$toolbar : false; + }, + addEvent: function(name) + { + this.core.event = name; + }, + getEvent: function() + { + return this.core.event; + }, + setCallback: function(type, e, data) + { + var callback = this.opts[type + 'Callback']; + if ($.isFunction(callback)) { - var $s = $(s); - var cloned = $s.clone(); - cloned.find('ul', 'ol').remove(); - - if (this.opts.linebreaks === false) - { - data += this.outerHtml($('<p>').append(cloned.contents())); - } - else - { - var clonedHtml = cloned.html().replace(/<br\s?\/?>$/i, ''); - data += clonedHtml + '<br>'; - } - - if (i == 0) - { - $s.addClass('redactor-replaced').empty(); - replaced = this.outerHtml($s); - } - else $s.remove(); + return (typeof data == 'undefined') ? callback.call(this, e) : callback.call(this, e, data); } + else + { + return (typeof data == 'undefined') ? e : data; + } + }, + destroy: function() + { + this.core.setCallback('destroy'); - }, this)); + // off events and remove data + this.$element.off('.redactor').removeData('redactor'); + this.$editor.off('.redactor'); + // common + this.$editor.removeClass('redactor-editor redactor-linebreaks redactor-placeholder'); + this.$editor.removeAttr('contenteditable'); - html = this.$editor.html().replace(replaced, '</' + listTag + '>' + data + '<' + listTag + '>'); + var html = this.code.get(); - this.$editor.html(html); - this.$editor.find(listTag + ':empty').remove(); - - } - - // insert lists - else - { - var firstParent = $(this.getParent()).closest('td'); - - if (this.browser('msie') && !this.isIe11() && this.opts.linebreaks) - { - var wrapper = this.selectionWrap('div'); - var wrapperHtml = $(wrapper).html(); - var tmpList = $('<ul>'); - if (cmd == 'insertorderedlist') + if (this.build.isTextarea()) { - tmpList = $('<ol>'); + this.$box.after(this.$element); + this.$box.remove(); + this.$element.val(html).show(); } - - var tmpLi = $('<li>'); - - if ($.trim(wrapperHtml) == '') - { - tmpLi.append(wrapperHtml + '<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>'); - tmpList.append(tmpLi); - this.$editor.find('#selection-marker-1').replaceWith(tmpList); - } else { - tmpLi.append(wrapperHtml); - tmpList.append(tmpLi); - $(wrapper).replaceWith(tmpList); + this.$box.after(this.$editor); + this.$box.remove(); + this.$element.html(html).show(); } - } - else - { - this.document.execCommand(cmd); - } - var parent = this.getParent(); - var $list = $(parent).closest('ol, ul'); + // paste box + if (this.$pasteBox) this.$pasteBox.remove(); - if (this.opts.linebreaks === false) - { - var listText = $.trim($list.text()); - if (listText == '') - { - $list.children('li').find('br').remove(); - $list.children('li').append('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>'); - } - } + // modal + if (this.$modalBox) this.$modalBox.remove(); + if (this.$modalOverlay) this.$modalOverlay.remove(); - if (firstParent.size() != 0) - { - $list.wrapAll('<td>'); - } + // buttons tooltip + $('.redactor-toolbar-tooltip').remove(); - if ($list.length) - { - // remove block-element list wrapper - var $listParent = $list.parent(); - if (this.isParentRedactor($listParent) && $listParent[0].tagName != 'LI' && this.nodeTestBlocks($listParent[0])) - { - $listParent.replaceWith($listParent.contents()); - } - } + // autosave + clearInterval(this.autosaveInterval); - if (this.browser('mozilla')) - { - this.$editor.focus(); } - } - - this.selectionRestore(); - this.$editor.find('#selection-marker-1').removeAttr('id'); - this.sync(); - this.callback('execCommand', cmd, param); - return; + }; }, - - // INDENTING - indentingIndent: function() + dropdown: function() { - this.indentingStart('indent'); - }, - indentingOutdent: function() - { - this.indentingStart('outdent'); - }, - indentingStart: function(cmd) - { - this.bufferSet(); + return { + build: function(name, $dropdown, dropdownObject) + { + if (name == 'formatting' && this.opts.formattingAdd) + { + $.each(this.opts.formattingAdd, $.proxy(function(i,s) + { + var name = s.tag; + if (typeof s.class != 'undefined') + { + name = name + '-' + s.class; + } - if (cmd === 'indent') - { - var block = this.getBlock(); + s.type = (this.utils.isBlockTag(s.tag)) ? 'block' : 'inline'; + var func = (s.type == 'inline') ? 'inline.formatting' : 'block.formatting'; - this.selectionSave(); + if (this.opts.linebreaks && s.type == 'block' && s.tag == 'p') return; - if (block && block.tagName == 'LI') - { - // li - var parent = this.getParent(); + this.formatting[name] = { + tag: s.tag, + style: s.style, + 'class': s.class, + attr: s.attr, + data: s.data, + clear: s.clear + }; - var $list = $(parent).closest('ol, ul'); - var listTag = $list[0].tagName; + dropdownObject[name] = { + func: func, + title: s.title + }; - var elems = this.getBlocks(); + }, this)); - $.each(elems, function(i,s) + } + + $.each(dropdownObject, $.proxy(function(btnName, btnObject) { - if (s.tagName == 'LI') + var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '">' + btnObject.title + '</a>'); + if (name == 'formatting') $item.addClass('redactor-formatting-' + btnName); + + $item.on('click', $.proxy(function(e) { - var $prev = $(s).prev(); - if ($prev.size() != 0 && $prev[0].tagName == 'LI') + var type = 'func'; + var callback = btnObject.func; + if (btnObject.command) { - var $childList = $prev.children('ul, ol'); - if ($childList.size() == 0) - { - $prev.append($('<' + listTag + '>').append(s)); - } - else $childList.append(s); + type = 'command'; + callback = btnObject.command; } - } - }); - } - // linebreaks - else if (block === false && this.opts.linebreaks === true) - { - this.exec('formatBlock', 'blockquote'); - var newblock = this.getBlock(); - var block = $('<div data-tagblock="">').html($(newblock).html()); - $(newblock).replaceWith(block); + else if (btnObject.dropdown) + { + type = 'dropdown'; + callback = btnObject.dropdown; + } - var left = this.normalize($(block).css('margin-left')) + this.opts.indentValue; - $(block).css('margin-left', left + 'px'); - } - else - { - // all block tags - var elements = this.getBlocks(); - $.each(elements, $.proxy(function(i, elem) - { - var $el = false; + this.button.onClick(e, btnName, type, callback); - if (elem.tagName === 'TD') return; + }, this)); - if ($.inArray(elem.tagName, this.opts.alignmentTags) !== -1) - { - $el = $(elem); - } - else - { - $el = $(elem).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]); - } + $dropdown.append($item); - var left = this.normalize($el.css('margin-left')) + this.opts.indentValue; - $el.css('margin-left', left + 'px'); - }, this)); - } + }, + show: function(e, key) + { + if (!this.opts.visual) + { + e.preventDefault(); + return false; + } - this.selectionRestore(); + var $button = this.button.get(key); - } - // outdent - else - { - this.selectionSave(); + // 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); - var block = this.getBlock(); - if (block && block.tagName == 'LI') - { - // li - var elems = this.getBlocks(); - var index = 0; + // ios keyboard hide + if (this.utils.isMobile() && !this.utils.browser('msie')) + { + document.activeElement.blur(); + } - this.insideOutdent(block, index, elems); - } - else - { - // all block tags - var elements = this.getBlocks(); - $.each(elements, $.proxy(function(i, elem) + if ($button.hasClass('dropact')) { - var $el = false; + this.dropdown.hideAll(); + } + else + { + this.dropdown.hideAll(); + this.core.setCallback('dropdownShow', { dropdown: $dropdown, key: key, button: $button }); - if ($.inArray(elem.tagName, this.opts.alignmentTags) !== -1) + this.button.setActive(key); + + $button.addClass('dropact'); + + var keyPosition = $button.offset(); + + // fix right placement + var dropdownWidth = $dropdown.width(); + if ((keyPosition.left + dropdownWidth) > $(document).width()) { - $el = $(elem); + keyPosition.left -= dropdownWidth; } - else - { - $el = $(elem).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]); - } - var left = this.normalize($el.css('margin-left')) - this.opts.indentValue; - if (left <= 0) + var left = keyPosition.left + 'px'; + if (this.$toolbar.hasClass('toolbar-fixed-box')) { - // linebreaks - if (this.opts.linebreaks === true && typeof($el.data('tagblock')) !== 'undefined') + var top = this.$toolbar.innerHeight() + this.opts.toolbarFixedTopOffset; + var position = 'fixed'; + if (this.opts.toolbarFixedTarget !== document) { - $el.replaceWith($el.html() + '<br>'); + top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top) + this.opts.toolbarFixedTopOffset; + position = 'absolute'; } - // all block tags - else - { - $el.css('margin-left', ''); - this.removeEmptyAttr($el, 'style'); - } + + $dropdown.css({ position: position, left: left, top: top + 'px' }).show(); } else { - $el.css('margin-left', left + 'px'); + var top = ($button.innerHeight() + keyPosition.top) + 'px'; + + $dropdown.css({ position: 'absolute', left: left, top: top }).show(); } - }, this)); - } + this.core.setCallback('dropdownShown', { dropdown: $dropdown, key: key, button: $button }); + } - this.selectionRestore(); - } + $(document).one('click', $.proxy(this.dropdown.hide, this)); + this.$editor.one('click', $.proxy(this.dropdown.hide, this)); - this.sync(); + // disable scroll whan dropdown scroll + var $body = $(document.body); + var width = $body.width(); - }, - insideOutdent: function (li, index, elems) - { - if (li && li.tagName == 'LI') - { - var $parent = $(li).parent().parent(); - if ($parent.size() != 0 && $parent[0].tagName == 'LI') + $dropdown.on('mouseover', function() { + + $body.addClass('body-redactor-hidden'); + $body.css('margin-right', ($body.width() - width) + 'px'); + + }); + + $dropdown.on('mouseout', function() { + + $body.removeClass('body-redactor-hidden').css('margin-right', 0); + + }); + + + e.stopPropagation(); + }, + hideAll: function() { - $parent.after(li); - } - else + 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) { - if (typeof elems[index] != 'undefined') + var $dropdown = $(e.target); + if (!$dropdown.hasClass('dropact')) { - li = elems[index]; - index++; - - this.insideOutdent(li, index, elems); + $dropdown.removeClass('dropact'); + this.dropdown.hideAll(); } - else - { - this.execCommand('insertunorderedlist'); - } } - } + }; }, - - // ALIGNMENT - alignmentLeft: function() + file: function() { - this.alignmentSet('', 'JustifyLeft'); - }, - alignmentRight: function() - { - this.alignmentSet('right', 'JustifyRight'); - }, - alignmentCenter: function() - { - this.alignmentSet('center', 'JustifyCenter'); - }, - alignmentJustify: function() - { - this.alignmentSet('justify', 'JustifyFull'); - }, - alignmentSet: function(type, cmd) - { - this.bufferSet(); + return { + show: function() + { + this.modal.load('file', this.lang.get('file'), 700); + this.upload.init('#redactor-modal-file-upload', this.opts.fileUpload, this.file.insert); - if (this.oldIE()) - { - this.document.execCommand(cmd, false, false); - return true; - } + this.selection.save(); - this.selectionSave(); + this.selection.get(); + var text = this.sel.toString(); - var block = this.getBlock(); - if (!block && this.opts.linebreaks) - { - // one element - this.exec('formatblock', 'div'); + $('#redactor-filename').val(text); - var newblock = this.getBlock(); - var block = $('<div data-tagblock="">').html($(newblock).html()); - $(newblock).replaceWith(block); - - $(block).css('text-align', type); - this.removeEmptyAttr(block, 'style'); - - if (type == '' && typeof($(block).data('tagblock')) !== 'undefined') + this.modal.show(); + }, + insert: function(json, direct, e) { - $(block).replaceWith($(block).html()); - } - } - else - { - var elements = this.getBlocks(); - $.each(elements, $.proxy(function(i, elem) - { - var $el = false; + // error callback + if (typeof json.error != 'undefined') + { + this.modal.close(); + this.selection.restore(); + this.core.setCallback('fileUploadError', json); + return; + } - if ($.inArray(elem.tagName, this.opts.alignmentTags) !== -1) + var link; + if (typeof json == 'string') { - $el = $(elem); + link = json; } else { - $el = $(elem).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]); + var text = $('#redactor-filename').val(); + if (typeof text == 'undefined' || text === '') text = json.filename; + + link = '<a href="' + json.filelink + '" id="filelink-marker">' + text + '</a>'; } - if ($el) + if (direct) { - $el.css('text-align', type); - this.removeEmptyAttr($el, 'style'); + this.selection.removeMarkers(); + var marker = this.selection.getMarker(); + this.insert.nodeToCaretPositionFromPoint(e, marker); } + else + { + this.modal.close(); + } - }, this)); - } + this.selection.restore(); + this.buffer.set(); - this.selectionRestore(); - this.sync(); - }, + this.insert.htmlWithoutClean(link); - // CLEAN - cleanEmpty: function(html) - { - var ph = this.placeholderStart(html); - if (ph !== false) return ph; + if (typeof json == 'string') return; - if (this.opts.linebreaks === false) - { - if (html === '') html = this.opts.emptyHtml; - else if (html.search(/^<hr\s?\/?>$/gi) !== -1) html = '<hr>' + this.opts.emptyHtml; - } + var linkmarker = $(this.$editor.find('a#filelink-marker')); + if (linkmarker.size() !== 0) + { + linkmarker.removeAttr('id').removeAttr('style'); + } + else linkmarker = false; - return html; - }, - cleanConverters: function(html) - { - // convert div to p - if (this.opts.convertDivs && !this.opts.gallery) - { - html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '<p$1>$2</p>'); - } + this.core.setCallback('fileUpload', linkmarker, json); - if (this.opts.paragraphy) html = this.cleanParagraphy(html); - - return html; + } + }; }, - cleanConvertProtected: function(html) + focus: function() { - if (this.opts.templateVars) - { - html = html.replace(/\{\{(.*?)\}\}/gi, '<!-- template double $1 -->'); - html = html.replace(/\{(.*?)\}/gi, '<!-- template $1 -->'); - } + return { + setStart: function() + { + this.$editor.focus(); - html = html.replace(/<script(.*?)>([\w\W]*?)<\/script>/gi, '<title type="text/javascript" style="display: none;" class="redactor-script-tag"$1>$2</title>'); - html = html.replace(/<style(.*?)>([\w\W]*?)<\/style>/gi, '<section$1 style="display: none;" rel="redactor-style-tag">$2</section>'); - html = html.replace(/<form(.*?)>([\w\W]*?)<\/form>/gi, '<section$1 rel="redactor-form-tag">$2</section>'); + var first = this.$editor.children().first(); - // php tags convertation - if (this.opts.phpTags) html = html.replace(/<\?php([\w\W]*?)\?>/gi, '<section style="display: none;" rel="redactor-php-tag">$1</section>'); - else html = html.replace(/<\?php([\w\W]*?)\?>/gi, ''); + if (first.size() === 0) return; + if (first[0].length === 0 || first[0].tagName == 'BR' || first[0].nodeType == 3) + { + return; + } - return html; - }, - cleanReConvertProtected: function(html) - { - if (this.opts.templateVars) - { - html = html.replace(/<!-- template double (.*?) -->/gi, '{{$1}}'); - html = html.replace(/<!-- template (.*?) -->/gi, '{$1}'); - } + if (first[0].tagName == 'UL' || first[0].tagName == 'OL') + { + first = first.find('li').first(); + var child = first.children().first(); + if (!this.utils.isBlock(child) && child.text() === '') + { + // empty inline tag in li + this.caret.setStart(child); + return; + } + } - html = html.replace(/<title type="text\/javascript" style="display: none;" class="redactor-script-tag"(.*?)>([\w\W]*?)<\/title>/gi, '<script$1 type="text/javascript">$2</script>'); - html = html.replace(/<section(.*?) style="display: none;" rel="redactor-style-tag">([\w\W]*?)<\/section>/gi, '<style$1>$2</style>'); - html = html.replace(/<section(.*?)rel="redactor-form-tag"(.*?)>([\w\W]*?)<\/section>/gi, '<form$1$2>$3</form>'); + if (this.opts.linebreaks && !this.utils.isBlockTag(first[0].tagName)) + { + this.selection.get(); + this.range.setStart(this.$editor[0], 0); + this.range.setEnd(this.$editor[0], 0); + this.selection.addRange(); - // php tags convertation - if (this.opts.phpTags) html = html.replace(/<section style="display: none;" rel="redactor-php-tag">([\w\W]*?)<\/section>/gi, '<?php\r\n$1\r\n?>'); + return; + } - return html; - }, - cleanRemoveSpaces: function(html, buffer) - { - if (buffer !== false) - { - var buffer = [] - var matches = html.match(/<(pre|style|script|title)(.*?)>([\w\W]*?)<\/(pre|style|script|title)>/gi); - if (matches === null) matches = []; - - if (this.opts.phpTags) + // if node is tag + this.caret.setStart(first); + }, + setEnd: function() { - var phpMatches = html.match(/<\?php([\w\W]*?)\?>/gi); - if (phpMatches) matches = $.merge(matches, phpMatches); - } - - if (matches) - { - $.each(matches, function(i, s) + if (this.utils.browser('mozilla') || this.utils.browser('msie')) { - html = html.replace(s, 'buffer_' + i); - buffer.push(s); - }); - } - } + var last = this.$editor.children().last(); + this.caret.setEnd(last); + } + else + { + this.selection.get(); - html = html.replace(/\n/g, ' '); - html = html.replace(/[\t]*/g, ''); - 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 + try { + this.range.selectNodeContents(this.$editor[0]); + this.range.collapse(false); - html = this.cleanReplacer(html, buffer); + this.selection.addRange(); + } + catch (e) {} + } - html = html.replace(/\n\n/g, "\n"); + }, + isFocused: function() + { + var focusNode = document.getSelection().focusNode; + if (focusNode === null) return false; - return html; + if (this.opts.linebreaks && $(focusNode.parentNode).hasClass('redactor-linebreaks')) return true; + else if (!this.utils.isRedactorParent(focusNode.parentNode)) return false; + + return this.$editor.is(':focus'); + } + }; }, - cleanReplacer: function(html, buffer) + image: function() { - if (buffer === false) return html; + return { + show: function() + { + this.modal.load('image', this.lang.get('image'), 700); + this.upload.init('#redactor-modal-image-droparea', this.opts.imageUpload, this.image.insert); - $.each(buffer, function(i,s) - { - html = html.replace('buffer_' + i, s); - }); + this.selection.save(); + this.modal.show(); - return html; - }, - cleanRemoveEmptyTags: function(html) - { - // remove zero width-space - html = html.replace(/[\u200B-\u200D\uFEFF]/g, ''); + }, + showEdit: function($image) + { + var $link = $image.closest('a'); - var etagsInline = ["<b>\\s*</b>", "<b>&nbsp;</b>", "<em>\\s*</em>"] - var etags = ["<pre></pre>", "<blockquote>\\s*</blockquote>", "<dd></dd>", "<dt></dt>", "<ul></ul>", "<ol></ol>", "<li></li>", "<table></table>", "<tr></tr>", "<span>\\s*<span>", "<span>&nbsp;<span>", "<p>\\s*</p>", "<p></p>", "<p>&nbsp;</p>", "<p>\\s*<br>\\s*</p>", "<div>\\s*</div>", "<div>\\s*<br>\\s*</div>"]; + this.modal.load('imageEdit', this.lang.get('edit'), 705); - if (this.opts.removeEmptyTags) - { - etags = etags.concat(etagsInline); - } - else etags = etagsInline; + this.modal.createCancelButton(); + this.image.buttonDelete = this.modal.createDeleteButton(this.lang.get('_delete')); + this.image.buttonSave = this.modal.createActionButton(this.lang.get('save')); - var len = etags.length; - for (var i = 0; i < len; ++i) - { - html = html.replace(new RegExp(etags[i], 'gi'), ""); - } + this.image.buttonDelete.on('click', $.proxy(function() + { + this.image.remove($image); - return html; - }, - cleanParagraphy: function(html) - { - html = $.trim(html); + }, this)); - if (this.opts.linebreaks === true) return html; - if (html === '' || html === '<p></p>') return this.opts.emptyHtml; + this.image.buttonSave.on('click', $.proxy(function() + { + this.image.update($image); - html = html + "\n"; + }, this)); - if (this.opts.removeEmptyTags === false) - { - return html; - } - var safes = []; - var matches = html.match(/<(table|div|pre|object)(.*?)>([\w\W]*?)<\/(table|div|pre|object)>/gi); - if (!matches) matches = []; + $('#redactor-image-title').val($image.attr('alt')); - var commentsMatches = html.match(/<!--([\w\W]*?)-->/gi); - if (commentsMatches) matches = $.merge(matches, commentsMatches); + if (!this.opts.imageLink) $('.redactor-image-link-option').hide(); + else + { + var $redactorImageLink = $('#redactor-image-link'); - if (this.opts.phpTags) - { - var phpMatches = html.match(/<section(.*?)rel="redactor-php-tag">([\w\W]*?)<\/section>/gi); - if (phpMatches) matches = $.merge(matches, phpMatches); - } + $redactorImageLink.attr('href', $image.attr('src')); + if ($link.size() !== 0) + { + $redactorImageLink.val($link.attr('href')); + if ($link.attr('target') == '_blank') $('#redactor-image-link-blank').prop('checked', true); + } + } - if (matches) - { - $.each(matches, function(i,s) + if (!this.opts.imagePosition) $('.redactor-image-position-option').hide(); + else + { + var floatValue = ($image.css('display') == 'block' && $image.css('float') == 'none') ? 'center' : $image.css('float'); + $('#redactor-image-align').val(floatValue); + } + + this.modal.show(); + + }, + setFloating: function($image) { - safes[i] = s; - html = html.replace(s, '{replace' + i + '}\n'); - }); - } + var floating = $('#redactor-image-align').val(); - html = html.replace(/<br \/>\s*<br \/>/gi, "\n\n"); - html = html.replace(/<br><br>/gi, "\n\n"); + var imageFloat = ''; + var imageDisplay = ''; + var imageMargin = ''; - function R(str, mod, r) - { - return html.replace(new RegExp(str, mod), r); - } + switch (floating) + { + case 'left': + imageFloat = 'left'; + imageMargin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0'; + break; + case 'right': + imageFloat = 'right'; + imageMargin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin; + break; + case 'center': + imageDisplay = 'block'; + imageMargin = 'auto'; + break; + } - var blocks = '(comment|html|body|head|title|meta|style|script|link|iframe|table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|option|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary)'; + $image.css({ 'float': imageFloat, display: imageDisplay, margin: imageMargin }); + $image.attr('rel', $image.attr('style')); + }, + update: function($image) + { + this.image.hideResize(); + this.buffer.set(); - html = R('(<' + blocks + '[^>]*>)', 'gi', "\n$1"); - html = R('(</' + blocks + '>)', 'gi', "$1\n\n"); - html = R("\r\n", 'g', "\n"); - html = R("\r", 'g', "\n"); - html = R("/\n\n+/", 'g', "\n\n"); + var $link = $image.closest('a'); - var htmls = html.split(new RegExp('\n\s*\n', 'g'), -1); + $image.attr('alt', $('#redactor-image-title').val()); - html = ''; - for (var i in htmls) - { - if (htmls.hasOwnProperty(i)) - { - if (htmls[i].search('{replace') == -1) + this.image.setFloating($image); + + // as link + var link = $.trim($('#redactor-image-link').val()); + if (link !== '') { - htmls[i] = htmls[i].replace(/<p>\n\t?<\/p>/gi, ''); - htmls[i] = htmls[i].replace(/<p><\/p>/gi, ''); + var target = ($('#redactor-image-link-blank').prop('checked')) ? true : false; - if (htmls[i] != '') + if ($link.size() === 0) { - html += '<p>' + htmls[i].replace(/^\n+|\n+$/g, "") + "</p>"; + var a = $('<a href="' + link + '">' + this.utils.getOuterHtml($image) + '</a>'); + if (target) a.attr('target', '_blank'); + + $image.replaceWith(a); } + else + { + $link.attr('href', link); + if (target) + { + $link.attr('target', '_blank'); + } + else + { + $link.removeAttr('target'); + } + } } - else html += htmls[i]; - } - } + else if ($link.size() !== 0) + { + $link.replaceWith(this.utils.getOuterHtml($image)); - html = R('<p><p>', 'gi', '<p>'); - html = R('</p></p>', 'gi', '</p>'); + } - html = R('<p>\s?</p>', 'gi', ''); + this.modal.close(); + this.observe.images(); + this.code.sync(); - html = R('<p>([^<]+)</(div|address|form)>', 'gi', "<p>$1</p></$2>"); - html = R('<p>(</?' + blocks + '[^>]*>)</p>', 'gi', "$1"); - html = R("<p>(<li.+?)</p>", 'gi', "$1"); - html = R('<p>\s?(</?' + blocks + '[^>]*>)', 'gi', "$1"); + }, + setEditable: function($image) + { + if (this.opts.imageEditable) + { + $image.on('dragstart', $.proxy(this.image.onDrag, this)); + } - html = R('(</?' + blocks + '[^>]*>)\s?</p>', 'gi', "$1"); - html = R('(</?' + blocks + '[^>]*>)\s?<br />', 'gi', "$1"); - html = R('<br />(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)[^>]*>)', 'gi', '$1'); - html = R("\n</p>", 'gi', '</p>'); + $image.on('mousedown', $.proxy(this.image.hideResize, this)); + $image.on('click touchstart', $.proxy(function(e) + { + this.observe.image = $image; - html = R('<li><p>', 'gi', '<li>'); - html = R('</p></li>', 'gi', '</li>'); - html = R('</li><p>', 'gi', '</li>'); - //html = R('</ul><p>(.*?)</li>', 'gi', '</ul></li>'); - // html = R('</ol><p>', 'gi', '</ol>'); - html = R('<p>\t?\n?<p>', 'gi', '<p>'); - html = R('</dt><p>', 'gi', '</dt>'); - html = R('</dd><p>', 'gi', '</dd>'); - html = R('<br></p></blockquote>', 'gi', '</blockquote>'); - html = R('<p>\t*</p>', 'gi', ''); + if (this.$editor.find('#redactor-image-box').size() !== 0) return false; - // restore safes - $.each(safes, function(i,s) - { - html = html.replace('{replace' + i + '}', s); - }); + this.image.resizer = this.image.loadEditableControls($image); - return $.trim(html); - }, - cleanConvertInlineTags: function(html, set) - { - var boldTag = 'strong'; - if (this.opts.boldTag === 'b') boldTag = 'b'; + $(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)); - var italicTag = 'em'; - if (this.opts.italicTag === 'i') italicTag = 'i'; + // resize + if (!this.opts.imageResizable) return; - html = html.replace(/<span style="font-style: italic;">([\w\W]*?)<\/span>/gi, '<' + italicTag + '>$1</' + italicTag + '>'); - html = html.replace(/<span style="font-weight: bold;">([\w\W]*?)<\/span>/gi, '<' + boldTag + '>$1</' + boldTag + '>'); + this.image.resizer.on('mousedown.redactor touchstart.redactor', $.proxy(function(e) + { + this.image.setResizable(e, $image); + }, this)); - // bold, italic, del - if (this.opts.boldTag === 'strong') html = html.replace(/<b>([\w\W]*?)<\/b>/gi, '<strong>$1</strong>'); - else html = html.replace(/<strong>([\w\W]*?)<\/strong>/gi, '<b>$1</b>'); - if (this.opts.italicTag === 'em') html = html.replace(/<i>([\w\W]*?)<\/i>/gi, '<em>$1</em>'); - else html = html.replace(/<em>([\w\W]*?)<\/em>/gi, '<i>$1</i>'); + }, this)); + }, + setResizable: function(e, $image) + { + e.preventDefault(); - html = html.replace(/<span style="text-decoration: underline;">([\w\W]*?)<\/span>/gi, '<u>$1</u>'); + this.image.resizeHandle = { + x : e.pageX, + y : e.pageY, + el : $image, + ratio: $image.width() / $image.height(), + h: $image.height() + }; - if (set !== true) html = html.replace(/<strike>([\w\W]*?)<\/strike>/gi, '<del>$1</del>'); - else html = html.replace(/<del>([\w\W]*?)<\/del>/gi, '<strike>$1</strike>'); + e = e.originalEvent || e; - return html; - }, - cleanStripTags: function(html) - { - if (html == '' || typeof html == 'undefined') return html; + if (e.targetTouches) + { + this.image.resizeHandle.x = e.targetTouches[0].pageX; + this.image.resizeHandle.y = e.targetTouches[0].pageY; + } - var allowed = false; - if (this.opts.allowedTags !== false) allowed = true; + this.image.startResize(); - var arr = allowed === true ? this.opts.allowedTags : this.opts.deniedTags; - var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi; - html = html.replace(tags, function ($0, $1) - { - if (allowed === true) return $.inArray($1.toLowerCase(), arr) > '-1' ? $0 : ''; - else return $.inArray($1.toLowerCase(), arr) > '-1' ? '' : $0; - }); + }, + startResize: function() + { + $(document).on('mousemove.redactor-image-resize touchmove.redactor-image-resize', $.proxy(this.image.moveResize, this)); + $(document).on('mouseup.redactor-image-resize touchend.redactor-image-resize', $.proxy(this.image.stopResize, this)); + }, + moveResize: function(e) + { + e.preventDefault(); - html = this.cleanConvertInlineTags(html); + e = e.originalEvent || e; - return html; + var height = this.image.resizeHandle.h; - }, - cleanSavePreCode: function(html, encode) - { - var pre = html.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/gi); - if (pre !== null) - { - $.each(pre, $.proxy(function(i,s) - { - var arr = s.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/i); + if (e.targetTouches) height += (e.targetTouches[0].pageY - this.image.resizeHandle.y); + else height += (e.pageY - this.image.resizeHandle.y); - arr[3] = arr[3].replace(/&nbsp;/g, ' '); + var width = Math.round(height * this.image.resizeHandle.ratio); - if (encode !== false) arr[3] = this.cleanEncodeEntities(arr[3]); + if (height < 50 || width < 100) return; - // $ fix - arr[3] = arr[3].replace(/\$/g, '&#36;'); + this.image.resizeHandle.el.width(width); + this.image.resizeHandle.el.height(this.image.resizeHandle.el.width()/this.image.resizeHandle.ratio); - html = html.replace(s, '<' + arr[1] + arr[2] + '>' + arr[3] + '</' + arr[1] + '>'); + this.code.sync(); + }, + stopResize: function() + { + this.handle = false; + $(document).off('.redactor-image-resize'); - }, this)); - } + this.image.hideResize(); + }, + onDrag: function(e) + { + if (this.$editor.find('#redactor-image-box').size() !== 0) + { + e.preventDefault(); + return false; + } - return html; - }, - cleanEncodeEntities: function(str) - { - str = String(str).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"'); - return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); - }, - cleanUnverified: function() - { - // label, abbr, mark, meter, code, q, dfn, ins, time, kbd, var - var $elem = this.$editor.find('li, img, a, b, strong, sub, sup, i, em, u, small, strike, del, span, cite'); + this.$editor.on('drop.redactor-image-inside-drop', $.proxy(function() + { + setTimeout($.proxy(this.image.onDrop, this), 1); - $elem.filter('[style*="background-color: transparent;"][style*="line-height"]') - .css('background-color', '') - .css('line-height', ''); + }, this)); + }, + onDrop: function() + { + this.image.fixImageSourceAfterDrop(); + this.observe.images(); + this.$editor.off('drop.redactor-image-inside-drop'); + this.clean.clearUnverified(); + this.code.sync(); + }, + fixImageSourceAfterDrop: function() + { + this.$editor.find('img[data-save-url]').each(function() + { + var $el = $(this); + $el.attr('src', $el.attr('data-save-url')); + $el.removeAttr('data-save-url'); + }); + }, + hideResize: function(e) + { + if (e && $(e.target).closest('#redactor-image-box').length !== 0) return; + if (e && e.target.tagName == 'IMG') + { + var $image = $(e.target); + $image.attr('data-save-url', $image.attr('src')); + } - $elem.filter('[style*="background-color: transparent;"]') - .css('background-color', ''); + var imageBox = this.$editor.find('#redactor-image-box'); + if (imageBox.size() === 0) return; - $elem.css('line-height', ''); + if (this.opts.imageEditable) + { + this.image.editter.remove(); + } - $.each($elem, $.proxy(function(i,s) - { - this.removeEmptyAttr(s, 'style'); - }, this)); + $(this.image.resizer).remove(); - var $elem2 = this.$editor.find('b, strong, i, em, u, strike, del'); - $elem2.css('font-size', ''); + imageBox.find('img').css({ + marginTop: imageBox[0].style.marginTop, + marginBottom: imageBox[0].style.marginBottom, + marginLeft: imageBox[0].style.marginLeft, + marginRight: imageBox[0].style.marginRight + }); - $.each($elem2, $.proxy(function(i,s) - { - this.removeEmptyAttr(s, 'style'); - }, this)); + imageBox.css('margin', ''); + imageBox.find('img').css('opacity', ''); + imageBox.replaceWith(function() + { + return $(this).contents(); + }); - // When we paste text in Safari is wrapping inserted div (remove it) - this.$editor.find('div[style="text-align: -webkit-auto;"]').contents().unwrap(); + $(document).off('click.redactor-image-resize-hide'); + this.$editor.off('click.redactor-image-resize-hide'); - // Remove all styles in ul, ol, li - this.$editor.find('ul, ol, li').removeAttr('style'); - }, + if (typeof this.image.resizeHandle !== 'undefined') + { + this.image.resizeHandle.el.attr('rel', this.image.resizeHandle.el.attr('style')); + } + this.code.sync(); - // TEXTAREA CODE FORMATTING - cleanHtml: function(code) - { - var i = 0, - codeLength = code.length, - point = 0, - start = null, - end = null, - tag = '', - out = '', - cont = ''; + }, + loadResizableControls: function($image, imageBox) + { + if (this.opts.imageResizable && !this.utils.isMobile()) + { + var imageResizer = $('<span id="redactor-image-resizer" data-redactor="verified"></span>'); - this.cleanlevel = 0; + if (!this.utils.isDesktop()) + { + imageResizer.css({ width: '15px', height: '15px' }); + } - for (; i < codeLength; i++) - { - point = i; + imageResizer.attr('contenteditable', false); + imageBox.append(imageResizer); + imageBox.append($image); - // if no more tags, copy and exit - if (-1 == code.substr(i).indexOf( '<' )) + return imageResizer; + } + else + { + imageBox.append($image); + return false; + } + }, + loadEditableControls: function($image) { - out += code.substr(i); + var imageBox = $('<span id="redactor-image-box" data-redactor="verified">'); + imageBox.css('float', $image.css('float')).attr('contenteditable', false); - return this.cleanFinish(out); - } + if ($image[0].style.margin != 'auto') + { + imageBox.css({ + marginTop: $image[0].style.marginTop, + marginBottom: $image[0].style.marginBottom, + marginLeft: $image[0].style.marginLeft, + marginRight: $image[0].style.marginRight + }); - // copy verbatim until a tag - while (point < codeLength && code.charAt(point) != '<') - { - point++; - } + $image.css('margin', ''); + } + else + { + imageBox.css({ 'display': 'block', 'margin': 'auto' }); + } - if (i != point) - { - cont = code.substr(i, point - i); - if (!cont.match(/^\s{2,}$/g)) + $image.css('opacity', '.5').after(imageBox); + + + if (this.opts.imageEditable) { - if ('\n' == out.charAt(out.length - 1)) out += this.cleanGetTabs(); - else if ('\n' == cont.charAt(0)) + // editter + this.image.editter = $('<span id="redactor-image-editter" data-redactor="verified">' + this.lang.get('edit') + '</span>'); + this.image.editter.attr('contenteditable', false); + this.image.editter.on('click', $.proxy(function() { - out += '\n' + this.cleanGetTabs(); - cont = cont.replace(/^\s+/, ''); - } + this.image.showEdit($image); + }, this)); - out += cont; + imageBox.append(this.image.editter); + + // position correction + var editerWidth = this.image.editter.innerWidth(); + this.image.editter.css('margin-left', '-' + editerWidth/2 + 'px'); } - if (cont.match(/\n/)) out += '\n' + this.cleanGetTabs(); - } + return this.image.loadResizableControls($image, imageBox); - start = point; - - // find the end of the tag - while (point < codeLength && '>' != code.charAt(point)) + }, + remove: function(image) { - point++; - } + var $image = $(image); + var $link = $image.closest('a'); + var $figure = $image.closest('figure'); + var $parent = $image.parent(); + if ($('#redactor-image-box').size() !== 0) + { + $parent = $('#redactor-image-box').parent(); + } - tag = code.substr(start, point - start); - i = point; + var $next; + if ($figure.size() !== 0) + { + $next = $figure.next(); + $figure.remove(); + } + else if ($link.size() !== 0) + { + $parent = $link.parent(); + $link.remove(); + } + else + { + $image.remove(); + } - var t; + $('#redactor-image-box').remove(); - if ('!--' == tag.substr(1, 3)) - { - if (!tag.match(/--$/)) + if ($figure.size() !== 0) { - while ('-->' != code.substr(point, 3)) - { - point++; - } - point += 2; - tag = code.substr(start, point - start); - i = point; + this.caret.setStart($next); } + else + { + this.caret.setStart($parent); + } - if ('\n' != out.charAt(out.length - 1)) out += '\n'; + // delete callback + this.core.setCallback('imageDelete', $image[0].src, $image); - out += this.cleanGetTabs(); - out += tag + '>\n'; - } - else if ('!' == tag[1]) + this.modal.close(); + this.code.sync(); + }, + insert: function(json, direct, e) { - out = this.placeTag(tag + '>', out); - } - else if ('?' == tag[1]) - { - out += tag + '>\n'; - } - else if (t = tag.match(/^<(script|style|pre)/i)) - { - t[1] = t[1].toLowerCase(); - tag = this.cleanTag(tag); - out = this.placeTag(tag, out); - end = String(code.substr(i + 1)).toLowerCase().indexOf('</' + t[1]); + // error callback + if (typeof json.error != 'undefined') + { + this.modal.close(); + this.selection.restore(); + this.core.setCallback('imageUploadError', json); + return; + } - if (end) + var $img; + if (typeof json == 'string') { - cont = code.substr(i + 1, end); - i += end; - out += cont; + $img = $(json).attr('data-redactor-inserted-image', 'true'); } - } - else - { - tag = this.cleanTag(tag); - out = this.placeTag(tag, out); - } - } + else + { + $img = $('<img>'); + $img.attr('src', json.filelink).attr('data-redactor-inserted-image', 'true'); + } - return this.cleanFinish(out); - }, - cleanGetTabs: function() - { - var s = ''; - for ( var j = 0; j < this.cleanlevel; j++ ) - { - s += '\t'; - } - return s; - }, - cleanFinish: function(code) - { - code = code.replace(/\n\s*\n/g, '\n'); - code = code.replace(/^[\s\n]*/, ''); - code = code.replace(/[\s\n]*$/, ''); - code = code.replace(/<script(.*?)>\n<\/script>/gi, '<script$1></script>'); + var node = $img; + var isP = this.utils.isCurrentOrParent('P'); + if (isP) + { + // will replace + node = $('<blockquote />').append($img); + } - this.cleanlevel = 0; + if (direct) + { + this.selection.removeMarkers(); + var marker = this.selection.getMarker(); + this.insert.nodeToCaretPositionFromPoint(e, marker); + } + else + { + this.modal.close(); + } - return code; - }, - cleanTag: function (tag) - { - var tagout = ''; - tag = tag.replace(/\n/g, ' '); - tag = tag.replace(/\s{2,}/g, ' '); - tag = tag.replace(/^\s+|\s+$/g, ' '); + this.selection.restore(); + this.buffer.set(); - var suffix = ''; - if (tag.match(/\/$/)) - { - suffix = '/'; - tag = tag.replace(/\/+$/, ''); - } - var m; - while (m = /\s*([^= ]+)(?:=((['"']).*?\3|[^ ]+))?/.exec(tag)) - { - if (m[2]) tagout += m[1].toLowerCase() + '=' + m[2]; - else if (m[1]) tagout += m[1].toLowerCase(); + this.insert.html(this.utils.getOuterHtml(node), false); - tagout += ' '; - tag = tag.substr(m[0].length); - } + var $image = this.$editor.find('img[data-redactor-inserted-image=true]').removeAttr('data-redactor-inserted-image'); - return tagout.replace(/\s*$/, '') + suffix + '>'; + if (isP) + { + $image.parent().contents().unwrap().wrap('<p />'); + } + else if (this.opts.linebreaks) + { + $image.before('<br>').after('<br>'); + } + + if (typeof json == 'string') return; + + this.core.setCallback('imageUpload', $image, json); + + } + }; }, - placeTag: function (tag, out) + indent: function() { - var nl = tag.match(this.cleannewLevel); - if (tag.match(this.cleanlineBefore) || nl) - { - out = out.replace(/\s*$/, ''); - out += '\n'; - } + return { + increase: function() + { + // focus + if (!this.utils.browser('msie')) this.$editor.focus(); - if (nl && '/' == tag.charAt(1)) this.cleanlevel--; - if ('\n' == out.charAt(out.length - 1)) out += this.cleanGetTabs(); - if (nl && '/' != tag.charAt(1)) this.cleanlevel++; + this.buffer.set(); + this.selection.save(); - out += tag; + var block = this.selection.getBlock(); - if (tag.match(this.cleanlineAfter) || tag.match(this.cleannewLevel)) - { - out = out.replace(/ *$/, ''); - out += '\n'; - } + if (block && block.tagName == 'LI') + { + this.indent.increaseLists(); + } + else if (block === false && this.opts.linebreaks) + { + this.indent.increaseText(); + } + else + { + this.indent.increaseBlocks(); + } - return out; - }, + this.selection.restore(); + this.code.sync(); + }, + increaseLists: function() + { + document.execCommand('indent'); - // FORMAT - formatEmpty: function(e) - { - var html = $.trim(this.$editor.html()); + this.indent.fixEmptyIndent(); + this.clean.normalizeLists(); + this.clean.clearUnverified(); + }, + increaseBlocks: function() + { + $.each(this.selection.getBlocks(), $.proxy(function(i, elem) + { + if (elem.tagName === 'TD' || elem.tagName === 'TH') return; - if (this.opts.linebreaks) - { - if (html == '') + var $el = this.utils.getAlignmentElement(elem); + + var left = this.utils.normalize($el.css('margin-left')) + this.opts.indentValue; + $el.css('margin-left', left + 'px'); + + }, this)); + }, + increaseText: function() { - e.preventDefault(); - this.$editor.html(''); - this.focus(); - } - } - else - { - html = html.replace(/<br\s?\/?>/i, ''); - var thtml = html.replace(/<p>\s?<\/p>/gi, ''); + var wrapper = this.selection.wrap('div'); + $(wrapper).attr('data-tagblock', 'redactor'); + $(wrapper).css('margin-left', this.opts.indentValue + 'px'); + }, + decrease: function() + { + this.buffer.set(); + this.selection.save(); - if (html === '' || thtml === '') + var block = this.selection.getBlock(); + if (block && block.tagName == 'LI') + { + this.indent.decreaseLists(); + } + else + { + this.indent.decreaseBlocks(); + } + + this.selection.restore(); + this.code.sync(); + }, + decreaseLists: function () { - e.preventDefault(); + document.execCommand('outdent'); - var node = $(this.opts.emptyHtml).get(0); - this.$editor.html(node); - this.focus(); - } - } + var current = this.selection.getCurrent(); - this.sync(); - }, - formatBlocks: function(tag) - { - if (this.browser('mozilla') && this.isFocused()) - { - this.$editor.focus(); - } + var $item = $(current).closest('li'); + var $parent = $item.parent(); + if ($item.size() !== 0 && $parent.size() !== 0 && $parent[0].tagName == 'LI') + { + $parent.after($item); + } - this.bufferSet(); + this.indent.fixEmptyIndent(); - var nodes = this.getBlocks(); - this.selectionSave(); + if (!this.opts.linebreaks && $item.size() === 0) + { + document.execCommand('formatblock', false, 'p'); + this.$editor.find('ul, ol, blockquote, p').each($.proxy(this.utils.removeEmpty, this)); + } - $.each(nodes, $.proxy(function(i, node) - { - if (node.tagName !== 'LI') + this.clean.clearUnverified(); + }, + decreaseBlocks: function() { - var parent = $(node).parent(); - - if (tag === 'p') + $.each(this.selection.getBlocks(), $.proxy(function(i, elem) { - if ((node.tagName === 'P' - && parent.size() != 0 - && parent[0].tagName === 'BLOCKQUOTE') - || - node.tagName === 'BLOCKQUOTE') + var $el = this.utils.getAlignmentElement(elem); + var left = this.utils.normalize($el.css('margin-left')) - this.opts.indentValue; + + if (left <= 0) { - this.formatQuote(); - return; - } - else if (this.opts.linebreaks) - { - if (node && node.tagName.search(/H[1-6]/) == 0) + if (this.opts.linebreaks && typeof($el.data('tagblock')) !== 'undefined') { - $(node).replaceWith(node.innerHTML + '<br>'); + $el.replaceWith($el.html() + '<br />'); } - else return; + else + { + $el.css('margin-left', ''); + this.utils.removeEmptyAttr($el, 'style'); + } } else { - this.formatBlock(tag, node); + $el.css('margin-left', left + 'px'); } - } - else + + }, this)); + }, + fixEmptyIndent: function() + { + var block = this.selection.getBlock(); + + if (this.range.collapsed && block && block.tagName == 'LI' && this.utils.isEmpty($(block).text())) { - this.formatBlock(tag, node); + var $block = $(block); + $block.find('span').not('.redactor-selection-marker').contents().unwrap(); + $block.append('<br>'); } } - - }, this)); - - this.selectionRestore(); - this.sync(); + }; }, - formatBlock: function(tag, block) + inline: function() { - if (block === false) block = this.getBlock(); - if (block === false && this.opts.linebreaks === true) - { - this.execCommand('formatblock', tag); - return true; - } + return { + formatting: function(name) + { + var type, value; - var contents = ''; - if (tag !== 'pre') - { - contents = $(block).contents(); - } - else - { - //contents = this.cleanEncodeEntities($(block).text()); - contents = $(block).html(); - if ($.trim(contents) === '') + if (typeof this.formatting[name].style != 'undefined') type = 'style'; + else if (typeof this.formatting[name].class != 'undefined') type = 'class'; + + if (type) value = this.formatting[name][type]; + + this.inline.format(this.formatting[name].tag, type, value); + + }, + format: function(tag, type, value) { - contents = '<span id="selection-marker-1"></span>'; - } - } + // Stop formatting pre and headers + if (this.utils.isCurrentOrParent('PRE') || this.utils.isCurrentOrParentHeader()) return; - if (block.tagName === 'PRE') tag = 'p'; + var tags = ['b', 'bold', 'i', 'italic', 'underline', 'strikethrough', 'deleted', 'superscript', 'subscript']; + var replaced = ['strong', 'strong', 'em', 'em', 'u', 'del', 'del', 'sup', 'sub']; - if (this.opts.linebreaks === true && tag === 'p') - { - $(block).replaceWith($('<div>').append(contents).html() + '<br>'); - } - else - { - var parent = this.getParent(); + for (var i = 0; i < tags.length; i++) + { + if (tag == tags[i]) tag = replaced[i]; + } - var node = $('<' + tag + '>').append(contents); - $(block).replaceWith(node); + this.inline.type = type || false; + this.inline.value = value || false; - if (parent && parent.tagName == 'TD') + this.buffer.set(); + this.$editor.focus(); + + this.selection.get(); + + if (this.range.collapsed) + { + this.inline.formatCollapsed(tag); + } + else + { + this.inline.formatMultiple(tag); + } + }, + formatCollapsed: function(tag) { - $(node).wrapAll('<td>'); - } - } - }, - formatChangeTag: function(fromElement, toTagName, save) - { - if (save !== false) this.selectionSave(); + var current = this.selection.getCurrent(); + var $parent = $(current).closest(tag + '[data-redactor-tag=' + tag + ']'); - var newElement = $('<' + toTagName + '/>'); - $(fromElement).replaceWith(function() { return newElement.append($(this).contents()); }); + // inline there is + if ($parent.size() !== 0) + { + this.caret.setAfter($parent[0]); - if (save !== false) this.selectionRestore(); + // remove empty + if (this.utils.isEmpty($parent.text())) $parent.remove(); - return newElement; - }, + this.code.sync(); - // QUOTE - formatQuote: function() - { - if (this.browser('mozilla') && this.isFocused()) - { - this.$editor.focus(); - } + return; + } - this.bufferSet(); + // create empty inline + var node = $('<' + tag + '>').attr('data-verified', 'redactor').attr('data-redactor-tag', tag); + node.html(this.opts.invisibleSpace); - // paragraphy - if (this.opts.linebreaks === false) - { - this.selectionSave(); + node = this.inline.setFormat(node); - var blocks = this.getBlocks(); + var node = this.insert.node(node); + this.caret.setEnd(node); - var blockquote = false; - var blocksLen = blocks.length; - if (blocks) + this.code.sync(); + }, + formatMultiple: function(tag) { - var data = ''; - var replaced = ''; - var replace = false; - var paragraphsOnly = true; + this.inline.formatConvert(tag); - $.each(blocks, function(i,s) - { - if (s.tagName !== 'P') paragraphsOnly = false; - }); + this.selection.save(); + document.execCommand('strikethrough'); - $.each(blocks, $.proxy(function(i,s) + + this.$editor.find('strike').each($.proxy(function(i,s) { - if (s.tagName === 'BLOCKQUOTE') + var $el = $(s); + + this.inline.formatRemoveSameChildren($el, tag); + + var $span; + if (this.inline.type) { - this.formatBlock('p', s, false); + $span = $('<span>').attr('data-redactor-tag', tag).attr('data-verified', 'redactor'); + $span = this.inline.setFormat($span); } - else if (s.tagName === 'P') + else { - blockquote = $(s).parent(); - // from blockquote - if (blockquote[0].tagName == 'BLOCKQUOTE') + $span = $('<' + tag + '>').attr('data-redactor-tag', tag).attr('data-verified', 'redactor'); + } + + $el.replaceWith($span.html($el.contents())); + + if (tag == 'span') + { + var $parent = $span.parent(); + if ($parent && $parent[0].tagName == 'SPAN' && this.inline.type == 'style') { - var count = $(blockquote).children('p').size(); + var arr = this.inline.value.split(';'); - // one - if (count == 1) + for (var z = 0; z < arr.length; z++) { - $(blockquote).replaceWith(s); - } - // all - else if (count == blocksLen) - { - replace = 'blockquote'; - data += this.outerHtml(s); - } - // some - else - { - replace = 'html'; - data += this.outerHtml(s); + if (arr[z] === '') return; + var style = arr[z].split(':'); + $parent.css(style[0], ''); - if (i == 0) + if (this.utils.removeEmptyAttr($parent, 'style')) { - $(s).addClass('redactor-replaced').empty(); - replaced = this.outerHtml(s); + $parent.replaceWith($parent.contents()); } - else $(s).remove(); + } - } - // to blockquote - else - { - if (paragraphsOnly === false || blocks.length == 1) - { - this.formatBlock('blockquote', s, false); - } - else - { - replace = 'paragraphs'; - data += this.outerHtml(s); - } - } + } } - else if (s.tagName !== 'LI') - { - this.formatBlock('blockquote', s, false); - } }, this)); - if (replace) + // clear text decoration + if (tag != 'span') { - if (replace == 'paragraphs') + this.$editor.find(this.opts.inlineTags.join(', ')).each($.proxy(function(i,s) { - $(blocks[0]).replaceWith('<blockquote>' + data + '</blockquote>'); - $(blocks).remove(); - } - else if (replace == 'blockquote') - { - $(blockquote).replaceWith(data); - } - else if (replace == 'html') - { - var html = this.$editor.html().replace(replaced, '</blockquote>' + data + '<blockquote>'); - - this.$editor.html(html); - this.$editor.find('blockquote').each(function() + var $el = $(s); + var property = $el.css('text-decoration'); + if (property == 'line-through') { - if ($.trim($(this).html()) == '') $(this).remove(); - }) - } + $el.css('text-decoration', ''); + this.utils.removeEmptyAttr($el, 'style'); + } + }, this)); } - } - this.selectionRestore(); - } - // linebreaks - else - { - var block = this.getBlock(); - if (block.tagName === 'BLOCKQUOTE') - { - this.selectionSave(); - - var html = $.trim($(block).html()); - var selection = $.trim(this.getSelectionHtml()); - - html = html.replace(/<span(.*?)id="selection-marker(.*?)<\/span>/gi, ''); - - if (html == selection) + if (tag != 'del') { - $(block).replaceWith($(block).html() + '<br>'); - } - else - { - // replace - this.inlineFormat('tmp'); - var tmp = this.$editor.find('tmp'); - tmp.empty(); - - var newhtml = this.$editor.html().replace('<tmp></tmp>', '</blockquote><span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>' + selection + '<blockquote>'); - - this.$editor.html(newhtml); - tmp.remove(); - this.$editor.find('blockquote').each(function() + var _this = this; + this.$editor.find('inline').each(function(i,s) { - if ($.trim($(this).html()) == '') $(this).remove(); - }) + _this.utils.replaceToTag(s, 'del'); + }); } - this.selectionRestore(); - this.$editor.find('span#selection-marker-1').attr('id', false); - } - else - { - var wrapper = this.selectionWrap('blockquote'); - var html = $(wrapper).html(); + this.selection.restore(); + this.code.sync(); - var blocksElemsRemove = ['ul', 'ol', 'table', 'tr', 'tbody', 'thead', 'tfoot', 'dl']; - $.each(blocksElemsRemove, function(i,s) + }, + formatRemoveSameChildren: function($el, tag) + { + $el.children(tag).each(function() { - html = html.replace(new RegExp('<' + s + '(.*?)>', 'gi'), ''); - html = html.replace(new RegExp('</' + s + '>', 'gi'), ''); + var $child = $(this); + if (!$child.hasClass('redactor-selection-marker')) + { + $child.contents().unwrap(); + } }); + }, + formatConvert: function(tag) + { + this.selection.save(); - var blocksElems = this.opts.blockLevelElements; - $.each(blocksElems, function(i,s) + var find = ''; + if (this.inline.type == 'class') find = '[data-redactor-class=' + this.inline.value + ']'; + else if (this.inline.type == 'style') { - html = html.replace(new RegExp('<' + s + '(.*?)>', 'gi'), ''); - html = html.replace(new RegExp('</' + s + '>', 'gi'), '<br>'); - }); + find = '[data-redactor-style="' + this.inline.value + '"]'; + } - $(wrapper).html(html); - this.selectionElement(wrapper); - var next = $(wrapper).next(); - if (next.size() != 0 && next[0].tagName === 'BR') + if (tag != 'del') { - next.remove(); + var self = this; + this.$editor.find('del').each(function(i,s) + { + self.utils.replaceToTag(s, 'inline'); + }); } - } - } - this.sync(); - }, + this.$editor.find('[data-redactor-tag="' + tag + '"]' + find).each(function() + { + if (find === '' && tag == 'span' && this.tagName.toLowerCase() == tag) return; - // BLOCK - blockRemoveAttr: function(attr, value) - { - var nodes = this.getBlocks(); - $(nodes).removeAttr(attr); + var $el = $(this); + $el.replaceWith($('<strike />').html($el.contents())); - this.sync(); - }, - blockSetAttr: function(attr, value) - { - var nodes = this.getBlocks(); - $(nodes).attr(attr, value); + }); - this.sync(); - }, - blockRemoveStyle: function(rule) - { - var nodes = this.getBlocks(); - $(nodes).css(rule, ''); - this.removeEmptyAttr(nodes, 'style'); + this.selection.restore(); + }, + setFormat: function(node) + { + switch (this.inline.type) + { + case 'class': - this.sync(); - }, - blockSetStyle: function (rule, value) - { - var nodes = this.getBlocks(); - $(nodes).css(rule, value); + if (node.hasClass(this.inline.value)) + { + node.removeClass(this.inline.value); + node.removeAttr('data-redactor-class'); + } + else + { + node.addClass(this.inline.value); + node.attr('data-redactor-class', this.inline.value); + } - this.sync(); - }, - blockRemoveClass: function(className) - { - var nodes = this.getBlocks(); - $(nodes).removeClass(className); - this.removeEmptyAttr(nodes, 'class'); - this.sync(); - }, - blockSetClass: function(className) - { - var nodes = this.getBlocks(); - $(nodes).addClass(className); + break; + case 'style': - this.sync(); - }, + node[0].style.cssText = this.inline.value; + node.attr('data-redactor-style', this.inline.value); - // INLINE - inlineRemoveClass: function(className) - { - this.selectionSave(); + break; + } - this.inlineEachNodes(function(node) - { - $(node).removeClass(className); - this.removeEmptyAttr(node, 'class'); - }); + return node; + }, + removeStyle: function() + { + this.buffer.set(); + var current = this.selection.getCurrent(); + var nodes = this.selection.getInlines(); - this.selectionRestore(); - this.sync(); - }, - inlineSetClass: function(className) - { - var current = this.getCurrent(); - if (!$(current).hasClass(className)) this.inlineMethods('addClass', className); - }, - inlineRemoveStyle: function (rule) - { - this.selectionSave(); + this.selection.save(); - this.inlineEachNodes(function(node) - { - $(node).css(rule, ''); - this.removeEmptyAttr(node, 'style'); - }); + if (current && current.tagName === 'SPAN') + { + var $s = $(current); - this.selectionRestore(); - this.sync(); - }, - inlineSetStyle: function(rule, value) - { - this.inlineMethods('css', rule, value); - }, - inlineRemoveAttr: function (attr) - { - this.selectionSave(); + $s.removeAttr('style'); + if ($s[0].attributes.length === 0) + { + $s.replaceWith($s.contents()); + } + } - var range = this.getRange(), node = this.getElement(), nodes = this.getNodes(); + $.each(nodes, $.proxy(function(i,s) + { + var $s = $(s); + if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) + { + $s.removeAttr('style'); + if ($s[0].attributes.length === 0) + { + $s.replaceWith($s.contents()); + } + } + }, this)); - if (range.collapsed || range.startContainer === range.endContainer && node) - { - nodes = $( node ); - } + this.selection.restore(); + this.code.sync(); - $(nodes).removeAttr(attr); + }, + removeStyleRule: function(name) + { + this.buffer.set(); + var parent = this.selection.getParent(); + var nodes = this.selection.getInlines(); - this.inlineUnwrapSpan(); + this.selection.save(); - this.selectionRestore(); - this.sync(); - }, - inlineSetAttr: function(attr, value) - { - this.inlineMethods('attr', attr, value ); - }, - inlineMethods: function(type, attr, value) - { - this.bufferSet(); - this.selectionSave(); + if (parent && parent.tagName === 'SPAN') + { + var $s = $(parent); - var range = this.getRange() - var el = this.getElement(); + $s.css(name, ''); + this.utils.removeEmptyAttr($s, 'style'); + if ($s[0].attributes.length === 0) + { + $s.replaceWith($s.contents()); + } + } - if ((range.collapsed || range.startContainer === range.endContainer) && el && !this.nodeTestBlocks(el)) - { - $(el)[type](attr, value); - } - else - { - var cmd, arg = value; - switch (attr) - { - case 'font-size': - cmd = 'fontSize'; - arg = 4; - break; - case 'font-family': - cmd = 'fontName'; - break; - case 'color': - cmd = 'foreColor'; - break; - case 'background-color': - cmd = 'backColor'; - break; - } + $.each(nodes, $.proxy(function(i,s) + { + var $s = $(s); + if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) + { + $s.css(name, ''); + this.utils.removeEmptyAttr($s, 'style'); + if ($s[0].attributes.length === 0) + { + $s.replaceWith($s.contents()); + } + } + }, this)); - this.document.execCommand(cmd, false, arg); - - var fonts = this.$editor.find('font'); - $.each(fonts, $.proxy(function(i, s) + this.selection.restore(); + this.code.sync(); + }, + removeFormat: function() { - this.inlineSetMethods(type, s, attr, value); + this.buffer.set(); + var current = this.selection.getCurrent(); - }, this)); + this.selection.save(); - } + document.execCommand('removeFormat'); - this.selectionRestore(); - this.sync(); - }, - inlineSetMethods: function(type, s, attr, value) - { - var parent = $(s).parent(), el; + if (current && current.tagName === 'SPAN') + { + $(current).replaceWith($(current).contents()); + } - var selectionHtml = this.getSelectionText(); - var parentHtml = $(parent).text(); - var selected = selectionHtml == parentHtml; - if (selected && parent && parent[0].tagName === 'INLINE' && parent[0].attributes.length != 0) - { - el = parent; - $(s).replaceWith($(s).html()); - } - else - { - el = $('<inline>').append($(s).contents()); - $(s).replaceWith(el); - } + $.each(this.selection.getNodes(), $.proxy(function(i,s) + { + var $s = $(s); + if ($.inArray(s.tagName.toLowerCase(), this.opts.inlineTags) != -1 && !$s.hasClass('redactor-selection-marker')) + { + $s.replaceWith($s.contents()); + } + }, this)); + this.selection.restore(); + this.code.sync(); - $(el)[type](attr, value); - - return el; + }, + toggleClass: function(className) + { + this.inline.format('span', 'class', className); + }, + toggleStyle: function(value) + { + this.inline.format('span', 'style', value); + } + }; }, - // Sort elements and execute callback - inlineEachNodes: function(callback) + insert: function() { - var range = this.getRange(), - node = this.getElement(), - nodes = this.getNodes(), - collapsed; - - if (range.collapsed || range.startContainer === range.endContainer && node) - { - nodes = $(node); - collapsed = true; - } - - $.each(nodes, $.proxy(function(i, node) - { - if (!collapsed && node.tagName !== 'INLINE') + return { + set: function(html, clean) { - var selectionHtml = this.getSelectionText(); - var parentHtml = $(node).parent().text(); - var selected = selectionHtml == parentHtml; + this.placeholder.remove(); - if (selected && node.parentNode.tagName === 'INLINE' && !$(node.parentNode).hasClass('redactor_editor')) + html = this.clean.setVerified(html); + + if (typeof clean == 'undefined') { - node = node.parentNode; + html = this.clean.onPaste(html, false); } - else return; - } - callback.call(this, node); - }, this ) ); - }, - inlineUnwrapSpan: function() - { - var $spans = this.$editor.find('inline'); + this.$editor.html(html); + this.selection.remove(); + this.focus.setEnd(); + this.clean.normalizeLists(); + this.code.sync(); + this.observe.load(); - $.each($spans, $.proxy(function(i, span) - { - var $span = $(span); - - if ($span.attr('class') === undefined && $span.attr('style') === undefined) + if (typeof clean == 'undefined') + { + setTimeout($.proxy(this.clean.clearUnverified, this), 10); + } + }, + text: function(text) { - $span.contents().unwrap(); - } + this.placeholder.remove(); - }, this)); - }, - inlineFormat: function(tag) - { - this.selectionSave(); + text = text.toString(); + text = $.trim(text); + text = this.clean.getPlainText(text, false); - this.document.execCommand('fontSize', false, 4 ); + this.$editor.focus(); - var fonts = this.$editor.find('font'); - var last; - $.each(fonts, function(i, s) - { - var el = $('<' + tag + '/>').append($(s).contents()); - $(s).replaceWith(el); - last = el; - }); + if (this.utils.browser('msie')) + { + this.insert.htmlIe(text); + } + else + { + this.selection.get(); - this.selectionRestore(); + this.range.deleteContents(); + var el = document.createElement("div"); + el.innerHTML = text; + var frag = document.createDocumentFragment(), node, lastNode; + while ((node = el.firstChild)) + { + lastNode = frag.appendChild(node); + } - this.sync(); - }, - inlineRemoveFormat: function(tag) - { - this.selectionSave(); + this.range.insertNode(frag); - var utag = tag.toUpperCase(); - var nodes = this.getNodes(); - var parent = $(this.getParent()).parent(); + if (lastNode) + { + var range = this.range.cloneRange(); + range.setStartAfter(lastNode); + range.collapse(true); + this.sel.removeAllRanges(); + this.sel.addRange(range); + } + } - $.each(nodes, function(i, s) - { - if (s.tagName === utag) this.inlineRemoveFormatReplace(s); - }); - - if (parent && parent[0].tagName === utag) this.inlineRemoveFormatReplace(parent); - - this.selectionRestore(); - this.sync(); - }, - inlineRemoveFormatReplace: function(el) - { - $(el).replaceWith($(el).contents()); - }, - - - // INSERT - insertHtml: function (html, sync) - { - var current = this.getCurrent(); - var parent = current.parentNode; - - this.focusWithSaveScroll(); - - this.bufferSet(); - - var $html = $('<div>').append($.parseHTML(html)); - html = $html.html(); - - html = this.cleanRemoveEmptyTags(html); - - // Update value - $html = $('<div>').append($.parseHTML(html)); - var currBlock = this.getBlock(); - - if ($html.contents().length == 1) - { - var htmlTagName = $html.contents()[0].tagName; - - // If the inserted and received text tags match - if (htmlTagName != 'P' && htmlTagName == currBlock.tagName || htmlTagName == 'PRE') + this.code.sync(); + this.clean.clearUnverified(); + }, + htmlWithoutClean: function(html) { - //html = $html.html(); - $html = $('<div>').append(html); - } - } + this.insert.html(html, false); + }, + html: function(html, clean) + { + this.placeholder.remove(); - if (this.opts.linebreaks) - { - html = html.replace(/<p(.*?)>([\w\W]*?)<\/p>/gi, '$2<br>'); - } + if (typeof clean == 'undefined') clean = true; - // add text in a paragraph - if (!this.opts.linebreaks && $html.contents().length == 1 && $html.contents()[0].nodeType == 3 - && (this.getRangeSelectedNodes().length > 2 || (!current || current.tagName == 'BODY' && !parent || parent.tagName == 'HTML'))) - { - html = '<p>' + html + '</p>'; - } + this.$editor.focus(); - html = this.setSpansVerifiedHtml(html); + html = this.clean.setVerified(html); - if ($html.contents().length > 1 && currBlock - || $html.contents().is('p, :header, ul, ol, li, div, table, td, blockquote, pre, address, section, header, footer, aside, article')) - { - if (this.browser('msie')) - { - if (!this.isIe11()) + if (clean) { - this.document.selection.createRange().pasteHTML(html); + html = this.clean.onPaste(html); } - else + + if (this.utils.browser('msie')) { - this.execPasteFrag(html); + this.insert.htmlIe(html); } - } - else - { - this.document.execCommand('inserthtml', false, html); - } - } - else this.insertHtmlAdvanced(html, false); + else + { + if (this.clean.singleLine) this.insert.execHtml(html); + else document.execCommand('insertHTML', false, html); - if (this.selectall) - { - this.window.setTimeout($.proxy(function() - { - if (!this.opts.linebreaks) this.selectionEnd(this.$editor.contents().last()); - else this.focusEnd(); + this.insert.htmlFixMozilla(); - }, this), 1); - } + } - this.observeStart(); + this.clean.normalizeLists(); - // set no editable - this.setNonEditable(); + // remove empty paragraphs finaly + if (!this.opts.linebreaks) + { + this.$editor.find('p').each($.proxy(this.utils.removeEmpty, this)); + } - if (sync !== false) this.sync(); - }, - insertHtmlAdvanced: function(html, sync) - { - html = this.setSpansVerifiedHtml(html); + this.code.sync(); + this.observe.load(); - var sel = this.getSelection(); + if (clean) + { + this.clean.clearUnverified(); + } - if (sel.getRangeAt && sel.rangeCount) - { - var range = sel.getRangeAt(0); - range.deleteContents(); - - var el = document.createElement('div'); - el.innerHTML = html; - var frag = document.createDocumentFragment(), node, lastNode; - while ((node = el.firstChild)) + }, + htmlFixMozilla: function() { - lastNode = frag.appendChild(node); - } + // FF inserts empty p when content was selected dblclick + if (!this.utils.browser('mozilla')) return; - range.insertNode(frag); + var $next = $(this.selection.getBlock()).next(); + if ($next.length > 0 && $next[0].tagName == 'P' && $next.html() === '') + { + $next.remove(); + } - if (lastNode) + }, + htmlIe: function(html) { - range = range.cloneRange(); - range.setStartAfter(lastNode); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - } - } + if (this.utils.isIe11()) + { + var parent = this.utils.isCurrentOrParent('P'); + var $html = $('<div>').append(html); + var blocksMatch = $html.contents().is('p, :header, dl, ul, ol, div, table, td, blockquote, pre, address, section, header, footer, aside, article'); - if (sync !== false) - { - this.sync(); - } + if (parent && blocksMatch) this.insert.ie11FixInserting(parent, html); + else this.insert.ie11PasteFrag(html); - }, - insertBeforeCursor: function(html) - { - html = this.setSpansVerifiedHtml(html); + return; + } - var node = $(html); + document.selection.createRange().pasteHTML(html); - var space = document.createElement("span"); - space.innerHTML = "\u200B"; + }, + execHtml: function(html) + { + html = this.clean.setVerified(html); - var range = this.getRange(); - range.insertNode(space); - range.insertNode(node[0]); - range.collapse(false); + this.selection.get(); - var sel = this.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); + this.range.deleteContents(); - this.sync(); - }, - insertText: function(html) - { - var $html = $($.parseHTML(html)); + var el = document.createElement('div'); + el.innerHTML = html; - if ($html.length) html = $html.text(); + var frag = document.createDocumentFragment(), node, lastNode; + while ((node = el.firstChild)) + { + lastNode = frag.appendChild(node); + } - this.focusWithSaveScroll(); + this.range.insertNode(frag); - if (this.browser('msie')) - { - if (!this.isIe11()) + this.range.collapse(true); + this.caret.setAfter(lastNode); + + }, + node: function(node, deleteContents) { - this.document.selection.createRange().pasteHTML(html); - } - else - { - this.execPasteFrag(html); - } - } - else - { - this.document.execCommand('inserthtml', false, html); - } + node = node[0] || node; - this.sync(); - }, - insertNode: function(node) - { - node = node[0] || node; + var html = this.utils.getOuterHtml(node); + html = this.clean.setVerified(html); - if (node.tagName == 'SPAN') - { - var replacementTag = 'inline'; + node = $(html)[0]; - var outer = node.outerHTML; + this.selection.get(); - // Replace opening tag - var regex = new RegExp('<' + node.tagName, 'i'); - var newTag = outer.replace(regex, '<' + replacementTag); + if (deleteContents !== false) + { + this.range.deleteContents(); + } - // Replace closing tag - regex = new RegExp('</' + node.tagName, 'i'); - newTag = newTag.replace(regex, '</' + replacementTag); - node = $(newTag)[0]; - } + this.range.insertNode(node); + this.range.collapse(false); + this.selection.addRange(); - var sel = this.getSelection(); - if (sel.getRangeAt && sel.rangeCount) - { - // with delete contents - range = sel.getRangeAt(0); - range.deleteContents(); - range.insertNode(node); - range.setEndAfter(node); - range.setStartAfter(node); - sel.removeAllRanges(); - sel.addRange(range); - } + return node; + }, + nodeToPoint: function(node, x, y) + { + node = node[0] || node; - return node; - }, - insertNodeToCaretPositionFromPoint: function(e, node) - { - var range; - var x = e.clientX, y = e.clientY; - if (this.document.caretPositionFromPoint) - { - var pos = this.document.caretPositionFromPoint(x, y); - range = this.getRange(); - range.setStart(pos.offsetNode, pos.offset); - range.collapse(true); - range.insertNode(node); - } - else if (this.document.caretRangeFromPoint) - { - range = this.document.caretRangeFromPoint(x, y); - range.insertNode(node); - } - else if (typeof document.body.createTextRange != "undefined") - { - range = this.document.body.createTextRange(); - range.moveToPoint(x, y); - var endRange = range.duplicate(); - endRange.moveToPoint(x, y); - range.setEndPoint("EndToEnd", endRange); - range.select(); - } + this.selection.get(); - }, - insertAfterLastElement: function(element, parent) - { - if (typeof(parent) != 'undefined') element = parent; + var range; + if (document.caretPositionFromPoint) + { + var pos = document.caretPositionFromPoint(x, y); - if (this.isEndOfElement()) - { - if (this.opts.linebreaks) - { - var contents = $('<div>').append($.trim(this.$editor.html())).contents(); - var last = contents.last()[0]; - if (last.tagName == 'SPAN' && last.innerHTML == '') + this.range.setStart(pos.offsetNode, pos.offset); + this.range.collapse(true); + this.range.insertNode(node); + } + else if (document.caretRangeFromPoint) { - last = contents.prev()[0]; + range = document.caretRangeFromPoint(x, y); + range.insertNode(node); } + else if (typeof document.body.createTextRange != "undefined") + { + range = document.body.createTextRange(); + range.moveToPoint(x, y); + var endRange = range.duplicate(); + endRange.moveToPoint(x, y); + range.setEndPoint("EndToEnd", endRange); + range.select(); + } + }, + nodeToCaretPositionFromPoint: function(e, node) + { + node = node[0] || node; - if (this.outerHtml(last) != this.outerHtml(element)) + var range; + var x = e.clientX, y = e.clientY; + if (document.caretPositionFromPoint) { - return false; + var pos = document.caretPositionFromPoint(x, y); + var sel = document.getSelection(); + range = sel.getRangeAt(0); + range.setStart(pos.offsetNode, pos.offset); + range.collapse(true); + range.insertNode(node); } - } - else + else if (document.caretRangeFromPoint) + { + range = document.caretRangeFromPoint(x, y); + range.insertNode(node); + } + else if (typeof document.body.createTextRange != "undefined") + { + range = document.body.createTextRange(); + range.moveToPoint(x, y); + var endRange = range.duplicate(); + endRange.moveToPoint(x, y); + range.setEndPoint("EndToEnd", endRange); + range.select(); + } + + }, + ie11FixInserting: function(parent, html) { - if (this.$editor.contents().last()[0] !== element) + var node = document.createElement('span'); + node.className = 'redactor-ie-paste'; + this.insert.node(node); + + var parHtml = $(parent).html(); + + parHtml = '<p>' + parHtml.replace(/<span class="redactor-ie-paste"><\/span>/gi, '</p>' + html + '<p>') + '</p>'; + $(parent).replaceWith(parHtml); + }, + ie11PasteFrag: function(html) + { + this.selection.get(); + this.range.deleteContents(); + + var el = document.createElement("div"); + el.innerHTML = html; + + var frag = document.createDocumentFragment(), node, lastNode; + while ((node = el.firstChild)) { - return false; + lastNode = frag.appendChild(node); } - } - this.insertingAfterLastElement(element); - } + this.range.insertNode(frag); + } + }; }, - insertingAfterLastElement: function(element) + keydown: function() { - this.bufferSet(); + return { + init: function(e) + { + if (this.rtePaste) return; - if (this.opts.linebreaks === false) - { - var node = $(this.opts.emptyHtml); - $(element).after(node); - this.selectionStart(node); - } - else - { - var node = $('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>', this.document)[0]; - $(element).after(node); - $(node).after(this.opts.invisibleSpace); - this.selectionRestore(); - this.$editor.find('span#selection-marker-1').removeAttr('id'); - } - }, - insertLineBreak: function(twice) - { - this.selectionSave(); + var key = e.which; + var arrow = (key >= 37 && key <= 40); - var br = '<br>'; - if (twice == true) - { - br = '<br><br>'; - } + this.keydown.ctrl = e.ctrlKey || e.metaKey; + this.keydown.current = this.selection.getCurrent(); + this.keydown.parent = this.selection.getParent(); + this.keydown.block = this.selection.getBlock(); - if (this.browser('mozilla')) - { - var span = $('<span>').html(this.opts.invisibleSpace); - this.$editor.find('#selection-marker-1').before(br).before(span).before(this.opts.invisibleSpace); + // detect tags + this.keydown.pre = this.utils.isTag(this.keydown.current, 'pre'); + this.keydown.blockquote = this.utils.isTag(this.keydown.current, 'blockquote'); + this.keydown.figcaption = this.utils.isTag(this.keydown.current, 'figcaption'); - this.setCaretAfter(span[0]); - span.remove(); + // shortcuts setup + this.shortcuts.init(e, key); - this.selectionRemoveMarkers(); - } - else - { - var parent = this.getParent(); - if (parent && parent.tagName === 'A') - { - var offset = this.getCaretOffset(parent); + this.keydown.checkEvents(arrow, key); + this.keydown.setupBuffer(e, key); + this.keydown.addArrowsEvent(arrow); + this.keydown.setupSelectAll(e, key); - var text = $.trim($(parent).text()).replace(/\n\r\n/g, ''); - var len = text.length; + // callback + var keydownStop = this.core.setCallback('keydown', e); + if (keydownStop === false) + { + e.preventDefault(); + return false; + } - if (offset == len) + // ie and ff exit from table + if (this.opts.enterKey && (this.utils.browser('msie') || this.utils.browser('mozilla')) && (key === this.keyCode.DOWN || key === this.keyCode.RIGHT)) { - this.selectionRemoveMarkers(); + var isEndOfTable = false; + var $table = false; + if (this.keydown.block && this.keydown.block.tagName === 'TD') + { + $table = $(this.keydown.block).closest('table'); + } - var node = $('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>', this.document)[0]; - $(parent).after(node); - $(node).before(br + (this.browser('webkit') ? this.opts.invisibleSpace : '')); - this.selectionRestore(); + if ($table && $table.find('td').last()[0] === this.keydown.block) + { + isEndOfTable = true; + } - return true; + if (this.utils.isEndOfElement() && isEndOfTable) + { + var node = $(this.opts.emptyHtml); + $table.after(node); + this.caret.setStart(node); + } } - } + // down + if (this.opts.enterKey && key === this.keyCode.DOWN) + { + this.keydown.onArrowDown(); + } - this.$editor.find('#selection-marker-1').before(br + (this.browser('webkit') ? this.opts.invisibleSpace : '')); - this.selectionRestore(); - } - }, - insertDoubleLineBreak: function() - { - this.insertLineBreak(true); - }, - replaceLineBreak: function(element) - { - var node = $('<br>' + this.opts.invisibleSpace); - $(element).replaceWith(node); - this.selectionStart(node); - }, + // turn off enter key + if (!this.opts.enterKey && key === this.keyCode.ENTER) + { + e.preventDefault(); + // remove selected + if (!this.range.collapsed) this.range.deleteContents(); + return; + } - // PASTE - pasteClean: function(html) - { - html = this.callback('pasteBefore', false, html); + // on enter + if (key == this.keyCode.ENTER && !e.shiftKey && !e.ctrlKey && !e.metaKey) + { + var stop = this.core.setCallback('enter', e); + if (stop === false) + { + e.preventDefault(); + return false; + } - // ie10 fix paste links - if (this.browser('msie')) - { - var tmp = $.trim(html); - if (tmp.search(/^<a(.*?)>(.*?)<\/a>$/i) == 0) - { - html = html.replace(/^<a(.*?)>(.*?)<\/a>$/i, "$2"); - } - } + if (this.keydown.blockquote && this.keydown.exitFromBlockquote(e) === true) + { + return false; + } - if (this.opts.pastePlainText) - { - var tmp = this.document.createElement('div'); + var current, $next; + if (this.keydown.pre) + { + return this.keydown.insertNewLine(e); + } + else if (this.keydown.blockquote || this.keydown.figcaption) + { + current = this.selection.getCurrent(); + $next = $(current).next(); - html = html.replace(/<br>|<\/H[1-6]>|<\/p>|<\/div>/gi, '\n'); + if ($next.size() !== 0 && $next[0].tagName == 'BR') + { + return this.keydown.insertBreakLine(e); + } + else if (this.utils.isEndOfElement() && (current && current != 'SPAN')) + { + return this.keydown.insertDblBreakLine(e); + } + else + { + return this.keydown.insertBreakLine(e); + } + } + else if (this.opts.linebreaks && !this.keydown.block) + { + current = this.selection.getCurrent(); + $next = $(this.keydown.current).next(); - tmp.innerHTML = html; - html = tmp.textContent || tmp.innerText; + if (current !== false && $(current).hasClass('redactor-invisible-space')) + { + $(current).remove(); + return this.keydown.insertDblBreakLine(e); + } + else + { + if ($next.length === 0 && current === false && typeof $next.context != 'undefined') + { + return this.keydown.insertDblBreakLine(e); + } - html = $.trim(html); - html = html.replace('\n', '<br>'); - html = this.cleanParagraphy(html); + 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') + { + setTimeout($.proxy(this.keydown.replaceDivToParagraph, this), 1); + } + else if (!this.opts.linebreaks && !this.keydown.block) + { + return this.keydown.insertParagraph(e); + } - this.pasteInsert(html); - return false; - } - // clean up table - var tablePaste = false; - if (this.currentOrParentIs('TD')) - { - tablePaste = true; - var blocksElems = this.opts.blockLevelElements; - blocksElems.push('tr'); - blocksElems.push('table'); - $.each(blocksElems, function(i,s) - { - html = html.replace(new RegExp('<' + s + '(.*?)>', 'gi'), ''); - html = html.replace(new RegExp('</' + s + '>', 'gi'), '<br>'); - }); - } + } - // clean up pre - if (this.currentOrParentIs('PRE')) - { - html = this.pastePre(html); - this.pasteInsert(html); - return true; - } - // ms words shapes - html = html.replace(/<img(.*?)v:shapes=(.*?)>/gi, ''); + // Shift+Enter or Ctrl+Enter + if (key === this.keyCode.ENTER && (e.ctrlKey || e.shiftKey)) + { + return this.keydown.onShiftEnter(e); + } - // ms word list - html = html.replace(/<p(.*?)class="MsoListParagraphCxSpFirst"([\w\W]*?)<\/p>/gi, '<ul><li$2</li>'); - html = html.replace(/<p(.*?)class="MsoListParagraphCxSpMiddle"([\w\W]*?)<\/p>/gi, '<li$2</li>'); - html = html.replace(/<p(.*?)class="MsoListParagraphCxSpLast"([\w\W]*?)<\/p>/gi, '<li$2</li></ul>'); - // one line - html = html.replace(/<p(.*?)class="MsoListParagraph"([\w\W]*?)<\/p>/gi, '<ul><li$2</li></ul>'); - // remove ms word's bullet - html = html.replace(/·/g, ''); - // remove comments and php tags - html = html.replace(/<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi, ''); + // tab or cmd + [ + if (key === this.keyCode.TAB || e.metaKey && key === 221 || e.metaKey && key === 219) + { + return this.keydown.onTab(e, key); + } - // remove nbsp - if (this.opts.cleanSpaces === true) - { - html = html.replace(/(&nbsp;){2,}/gi, '&nbsp;'); - html = html.replace(/&nbsp;/gi, ' '); - } - // remove google docs marker - html = html.replace(/<b\sid="internal-source-marker(.*?)">([\w\W]*?)<\/b>/gi, "$2"); - html = html.replace(/<b(.*?)id="docs-internal-guid(.*?)">([\w\W]*?)<\/b>/gi, "$3"); + // 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) + { + var self = this; + $.each(children, function(z,s) + { + var $s = $(s); + if ($s.css('float') != 'none') return; + // image delete callback + self.core.setCallback('imageDelete', s.src, $s); + last = s; + }); + } + else if (nodes[i].tagName == 'IMG') + { + if (last != nodes[i]) + { + // image delete callback + this.core.setCallback('imageDelete', nodes[i].src, $(nodes[i])); + last = nodes[i]; + } + } + } + } + } - html = html.replace(/<span[^>]*(font-style: italic; font-weight: bold|font-weight: bold; font-style: italic)[^>]*>/gi, '<span style="font-weight: bold;"><span style="font-style: italic;">'); - html = html.replace(/<span[^>]*font-style: italic[^>]*>/gi, '<span style="font-style: italic;">'); - html = html.replace(/<span[^>]*font-weight: bold[^>]*>/gi, '<span style="font-weight: bold;">'); - html = html.replace(/<span[^>]*text-decoration: underline[^>]*>/gi, '<span style="text-decoration: underline;">'); + // backspace + if (key === this.keyCode.BACKSPACE) + { + this.keydown.removeInvisibleSpace(); + this.keydown.removeEmptyListInTable(e); + } - // strip tags - //html = this.cleanStripTags(html); + this.code.sync(); + }, + checkEvents: function(arrow, key) + { + if (!arrow && (this.core.getEvent() == 'click' || this.core.getEvent() == 'arrow')) + { + this.core.addEvent(false); + if (this.keydown.checkKeyEvents(key)) + { + this.buffer.set(); + } + } + }, + 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]; + return ($.inArray(key, keys) == -1) ? true : false; - // prevert - html = html.replace(/<td>\u200b*<\/td>/gi, '[td]'); - html = html.replace(/<td>&nbsp;<\/td>/gi, '[td]'); - html = html.replace(/<td><br><\/td>/gi, '[td]'); - html = html.replace(/<td(.*?)colspan="(.*?)"(.*?)>([\w\W]*?)<\/td>/gi, '[td colspan="$2"]$4[/td]'); - html = html.replace(/<td(.*?)rowspan="(.*?)"(.*?)>([\w\W]*?)<\/td>/gi, '[td rowspan="$2"]$4[/td]'); - html = html.replace(/<a(.*?)href="(.*?)"(.*?)>([\w\W]*?)<\/a>/gi, '[a href="$2"]$4[/a]'); - html = html.replace(/<iframe(.*?)>([\w\W]*?)<\/iframe>/gi, '[iframe$1]$2[/iframe]'); - html = html.replace(/<video(.*?)>([\w\W]*?)<\/video>/gi, '[video$1]$2[/video]'); - html = html.replace(/<audio(.*?)>([\w\W]*?)<\/audio>/gi, '[audio$1]$2[/audio]'); - html = html.replace(/<embed(.*?)>([\w\W]*?)<\/embed>/gi, '[embed$1]$2[/embed]'); - html = html.replace(/<object(.*?)>([\w\W]*?)<\/object>/gi, '[object$1]$2[/object]'); - html = html.replace(/<param(.*?)>/gi, '[param$1]'); + }, + addArrowsEvent: function(arrow) + { + if (!arrow) return; - html = html.replace(/<img(.*?)>/gi, '[img$1]'); + if ((this.core.getEvent() == 'click' || this.core.getEvent() == 'arrow')) + { + this.core.addEvent(false); + return; + } - // remove classes - html = html.replace(/ class="(.*?)"/gi, ''); + this.core.addEvent('arrow'); + }, + setupBuffer: function(e, key) + { + if (this.keydown.ctrl && key === 90 && !e.shiftKey && !e.altKey && this.opts.buffer.length) // z key + { + e.preventDefault(); + this.buffer.undo(); + return; + } + // undo + else if (this.keydown.ctrl && key === 90 && e.shiftKey && !e.altKey && this.opts.rebuffer.length !== 0) + { + e.preventDefault(); + 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) + { + this.buffer.set(); + } + } + }, + setupSelectAll: function(e, key) + { + if (this.keydown.ctrl && key === 65) + { + this.utils.enableSelectAll(); + } + else if (key != this.keyCode.LEFT_WIN && !this.keydown.ctrl) + { + this.utils.disableSelectAll(); + } + }, + onArrowDown: function() + { + var tags = [this.keydown.blockquote, this.keydown.pre, this.keydown.figcaption]; - // remove all attributes - html = html.replace(/<(\w+)([\w\W]*?)>/gi, '<$1>'); + for (var i = 0; i < tags.length; i++) + { + if (tags[i]) + { + this.keydown.insertAfterLastElement(tags[i]); + return false; + } + } + }, + onShiftEnter: function(e) + { + this.buffer.set(); - // remove empty - if (this.opts.linebreaks) - { - // prevent double linebreaks when an empty line in RTF has bold or underlined formatting associated with it - html = html.replace(/<strong><\/strong>/gi, ''); - html = html.replace(/<u><\/u>/gi, ''); + if (this.utils.isEndOfElement()) + { + return this.keydown.insertDblBreakLine(e); + } - if (this.opts.cleanFontTag) + return this.keydown.insertBreakLine(e); + }, + onTab: function(e, key) { - html = html.replace(/<font(.*?)>([\w\W]*?)<\/font>/gi, '$2'); - } + if (!this.opts.tabKey) return true; + if (this.utils.isEmpty(this.code.get()) && this.opts.tabAsSpaces === false) return true; - html = html.replace(/<[^\/>][^>]*>(\s*|\t*|\n*|&nbsp;|<br>)<\/[^>]+>/gi, '<br>'); - } - else - { - html = html.replace(/<[^\/>][^>]*>(\s*|\t*|\n*|&nbsp;|<br>)<\/[^>]+>/gi, ''); - } + e.preventDefault(); - html = html.replace(/<div>\s*?\t*?\n*?(<ul>|<ol>|<p>)/gi, '$1'); + var node; + if (this.keydown.pre && !e.shiftKey) + { + node = (this.opts.preSpaces) ? document.createTextNode(Array(this.opts.preSpaces + 1).join('\u00a0')) : document.createTextNode('\t'); + this.insert.node(node); + this.code.sync(); + } + else if (this.opts.tabAsSpaces !== false) + { + node = document.createTextNode(Array(this.opts.tabAsSpaces + 1).join('\u00a0')); + this.insert.node(node); + this.code.sync(); + } + else + { + if (e.metaKey && key === 219) this.indent.decrease(); + else if (e.metaKey && key === 221) this.indent.increase(); + else if (!e.shiftKey) this.indent.increase(); + else this.indent.decrease(); + } - // revert - html = html.replace(/\[td colspan="(.*?)"\]([\w\W]*?)\[\/td\]/gi, '<td colspan="$1">$2</td>'); - html = html.replace(/\[td rowspan="(.*?)"\]([\w\W]*?)\[\/td\]/gi, '<td rowspan="$1">$2</td>'); - html = html.replace(/\[td\]/gi, '<td>&nbsp;</td>'); - html = html.replace(/\[a href="(.*?)"\]([\w\W]*?)\[\/a\]/gi, '<a href="$1">$2</a>'); - html = html.replace(/\[iframe(.*?)\]([\w\W]*?)\[\/iframe\]/gi, '<iframe$1>$2</iframe>'); - html = html.replace(/\[video(.*?)\]([\w\W]*?)\[\/video\]/gi, '<video$1>$2</video>'); - html = html.replace(/\[audio(.*?)\]([\w\W]*?)\[\/audio\]/gi, '<audio$1>$2</audio>'); - html = html.replace(/\[embed(.*?)\]([\w\W]*?)\[\/embed\]/gi, '<embed$1>$2</embed>'); - html = html.replace(/\[object(.*?)\]([\w\W]*?)\[\/object\]/gi, '<object$1>$2</object>'); - html = html.replace(/\[param(.*?)\]/gi, '<param$1>'); - html = html.replace(/\[img(.*?)\]/gi, '<img$1>'); + return false; + }, + replaceDivToBreakLine: function() + { + var blockElem = this.selection.getBlock(); + var blockHtml = blockElem.innerHTML.replace(/<br\s?\/?>/gi, ''); + if ((blockElem.tagName === 'DIV' || blockElem.tagName === 'P') && blockHtml === '' && !$(blockElem).hasClass('redactor-editor')) + { + var br = document.createElement('br'); - // convert div to p - if (this.opts.convertDivs) - { - html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '<p>$2</p>'); - html = html.replace(/<\/div><p>/gi, '<p>'); - html = html.replace(/<\/p><\/div>/gi, '</p>'); - html = html.replace(/<p><\/p>/gi, '<br />'); - } - else - { - html = html.replace(/<div><\/div>/gi, '<br />'); - } + $(blockElem).replaceWith(br); + this.caret.setBefore(br); - // strip tags - html = this.cleanStripTags(html); + this.code.sync(); - if (this.currentOrParentIs('LI')) - { - html = html.replace(/<p>([\w\W]*?)<\/p>/gi, '$1<br>'); - } - else if (tablePaste === false) - { - html = this.cleanParagraphy(html); - } + return false; + } + }, + replaceDivToParagraph: function() + { + var blockElem = this.selection.getBlock(); + var blockHtml = blockElem.innerHTML.replace(/<br\s?\/?>/gi, ''); + if (blockElem.tagName === 'DIV' && blockHtml === '' && !$(blockElem).hasClass('redactor-editor')) + { + var p = document.createElement('p'); + p.innerHTML = this.opts.invisibleSpace; - // remove span - html = html.replace(/<span(.*?)>([\w\W]*?)<\/span>/gi, '$2'); + $(blockElem).replaceWith(p); + this.caret.setStart(p); - // remove empty - html = html.replace(/<img>/gi, ''); - html = html.replace(/<[^\/>][^>][^img|param|source|td][^<]*>(\s*|\t*|\n*| |<br>)<\/[^>]+>/gi, ''); + this.code.sync(); - html = html.replace(/\n{3,}/gi, '\n'); + return false; + } + else if (this.opts.cleanStyleOnEnter && blockElem.tagName == 'P') + { + $(blockElem).removeAttr('class').removeAttr('style'); + } + }, + insertParagraph: function(e) + { + e.preventDefault(); - // remove dirty p - html = html.replace(/<p><p>/gi, '<p>'); - html = html.replace(/<\/p><\/p>/gi, '</p>'); + this.selection.get(); - html = html.replace(/<li>(\s*|\t*|\n*)<p>/gi, '<li>'); - html = html.replace(/<\/p>(\s*|\t*|\n*)<\/li>/gi, '</li>'); + var p = document.createElement('p'); + p.innerHTML = this.opts.invisibleSpace; - if (this.opts.linebreaks === true) - { - html = html.replace(/<p(.*?)>([\w\W]*?)<\/p>/gi, '$2<br>'); - } + this.range.deleteContents(); + this.range.insertNode(p); - // remove empty finally - html = html.replace(/<[^\/>][^>][^img|param|source|td][^<]*>(\s*|\t*|\n*| |<br>)<\/[^>]+>/gi, ''); + this.caret.setStart(p); - // remove safari local images - html = html.replace(/<img src="webkit-fake-url\:\/\/(.*?)"(.*?)>/gi, ''); + this.code.sync(); - // remove p in td - html = html.replace(/<td(.*?)>(\s*|\t*|\n*)<p>([\w\W]*?)<\/p>(\s*|\t*|\n*)<\/td>/gi, '<td$1>$3</td>'); - - // remove divs - if (this.opts.convertDivs) - { - html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '$2'); - html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '$2'); - } - - // FF specific - this.pasteClipboardMozilla = false; - if (this.browser('mozilla')) - { - if (this.opts.clipboardUpload) + return false; + }, + exitFromBlockquote: function(e) { - var matches = html.match(/<img src="data:image(.*?)"(.*?)>/gi); - if (matches !== null) + if (!this.utils.isEndOfElement()) return; + + var tmp = $.trim($(this.keydown.block).html()); + if (tmp.search(/(<br\s?\/?>){2}$/i) != -1) { - this.pasteClipboardMozilla = matches; - for (k in matches) + e.preventDefault(); + + if (this.opts.linebreaks) { - var img = matches[k].replace('<img', '<img data-mozilla-paste-image="' + k + '" '); - html = html.replace(matches[k], img); + var br = document.createElement('br'); + $(this.keydown.blockquote).after(br); + + this.caret.setBefore(br); + $(this.keydown.block).html(tmp.replace(/<br\s?\/?>$/i, '')); } + else + { + var node = $(this.opts.emptyHtml); + $(this.keydown.blockquote).after(node); + this.caret.setStart(node); + } + + return true; + } - } - // FF fix - while (/<br>$/gi.test(html)) + return; + + }, + insertAfterLastElement: function(element) { - html = html.replace(/<br>$/gi, ''); - } - } + if (!this.utils.isEndOfElement()) return; - // bullets again - html = html.replace(/<p>•([\w\W]*?)<\/p>/gi, '<li>$1</li>'); + this.buffer.set(); - // ie inserts a blank font tags when pasting - if (this.browser('msie')) - { - while (/<font>([\w\W]*?)<\/font>/gi.test(html)) - { - html = html.replace(/<font>([\w\W]*?)<\/font>/gi, '$1'); - } - } + if (this.opts.linebreaks) + { + var contents = $('<div>').append($.trim(this.$editor.html())).contents(); + var last = contents.last()[0]; + if (last.tagName == 'SPAN' && last.innerHTML === '') + { + last = contents.prev()[0]; + } - // remove table paragraphs - if (tablePaste === false) - { - html = html.replace(/<td(.*?)>([\w\W]*?)<p(.*?)>([\w\W]*?)<\/td>/gi, '<td$1>$2$4</td>'); - html = html.replace(/<td(.*?)>([\w\W]*?)<\/p>([\w\W]*?)<\/td>/gi, '<td$1>$2$3</td>'); - html = html.replace(/<td(.*?)>([\w\W]*?)<p(.*?)>([\w\W]*?)<\/td>/gi, '<td$1>$2$4</td>'); - html = html.replace(/<td(.*?)>([\w\W]*?)<\/p>([\w\W]*?)<\/td>/gi, '<td$1>$2$3</td>'); - } + if (this.utils.getOuterHtml(last) != this.utils.getOuterHtml(element)) return; - // ms word break lines - html = html.replace(/\n/g, ' '); + var br = document.createElement('br'); + $(element).after(br); + this.caret.setAfter(br); - // ms word lists break lines - html = html.replace(/<p>\n?<li>/gi, '<li>'); + } + else + { + if (this.$editor.contents().last()[0] !== element) return; - this.pasteInsert(html); + var node = $(this.opts.emptyHtml); + $(element).after(node); + this.caret.setStart(node); + } + }, + insertNewLine: function(e) + { + e.preventDefault(); - }, - pastePre: function(s) - { - s = s.replace(/<br>|<\/H[1-6]>|<\/p>|<\/div>/gi, '\n'); + var node = document.createTextNode('\n'); - var tmp = this.document.createElement('div'); - tmp.innerHTML = s; - return this.cleanEncodeEntities(tmp.textContent || tmp.innerText); - }, - pasteInsert: function(html) - { - html = this.callback('pasteAfter', false, html); + this.selection.get(); - if (this.selectall) - { - this.$editor.html(html); - this.selectionRemove(); - this.focusEnd(); - this.sync(); - } - else - { - this.insertHtml(html); - } + this.range.deleteContents(); + this.range.insertNode(node); - this.selectall = false; + this.caret.setAfter(node); - setTimeout($.proxy(function() - { - this.rtePaste = false; + this.code.sync(); - // FF specific - if (this.browser('mozilla')) + return false; + }, + insertBreakLine: function(e) { - this.$editor.find('p:empty').remove() - } - if (this.pasteClipboardMozilla !== false) + return this.keydown.insertBreakLineProcessing(e); + }, + insertDblBreakLine: function(e) { - this.pasteClipboardUploadMozilla(); - } - - }, this), 100); - - if (this.opts.autoresize && this.fullscreen !== true) - { - $(this.document.body).scrollTop(this.saveScroll); - } - else - { - this.$editor.scrollTop(this.saveScroll); - } - }, - pasteClipboardAppendFields: function(postData) - { - // append hidden fields - if (this.opts.uploadFields !== false && typeof this.opts.uploadFields === 'object') - { - $.each(this.opts.uploadFields, $.proxy(function(k, v) + return this.keydown.insertBreakLineProcessing(e, true); + }, + insertBreakLineProcessing: function(e, dbl) { - if (v != null && v.toString().indexOf('#') === 0) v = $(v).val(); - postData[k] = v; + e.stopPropagation(); - }, this)); - } + this.selection.get(); + var br1 = document.createElement('br'); - return postData; - }, - pasteClipboardUploadMozilla: function() - { - var imgs = this.$editor.find('img[data-mozilla-paste-image]'); - $.each(imgs, $.proxy(function(i,s) - { - var $s = $(s); - var arr = s.src.split(","); - var postData = { - 'contentType': arr[0].split(";")[0].split(":")[1], - 'data': arr[1] // raw base64 - }; + this.range.deleteContents(); + this.range.insertNode(br1); - // append hidden fields - postData = this.pasteClipboardAppendFields(postData); + if (dbl === true) + { + var br2 = document.createElement('br'); + this.range.insertNode(br2); + this.caret.setAfter(br2); + } + else + { + this.caret.setAfter(br1); + } - $.post(this.opts.clipboardUploadUrl, postData, - $.proxy(function(data) + this.code.sync(); + + return false; + }, + removeInvisibleSpace: function() { - var json = (typeof data === 'string' ? $.parseJSON(data) : data); - $s.attr('src', json.filelink); - $s.removeAttr('data-mozilla-paste-image'); + var $current = $(this.keydown.current); + if ($current.text().search(/^\u200B$/g) === 0) + { + $current.remove(); + } + }, + removeEmptyListInTable: function(e) + { + var $current = $(this.keydown.current); + var $parent = $(this.keydown.parent); + var td = $current.closest('td'); - this.sync(); + if (td.size() !== 0 && $current.closest('li') && $parent.children('li').size() === 1) + { + if (!this.utils.isEmpty($current.text())) return; - // upload callback - this.callback('imageUpload', $s, json); + e.preventDefault(); - }, this)); + $current.remove(); + $parent.remove(); - }, this)); + this.caret.setStart(td); + } + } + }; }, - pasteClipboardUpload: function(e) + keyup: function() { - var result = e.target.result; - var arr = result.split(","); - var postData = { - 'contentType': arr[0].split(";")[0].split(":")[1], - 'data': arr[1] // raw base64 - }; + return { + init: function(e) + { + if (this.rtePaste) return; + var key = e.which; - if (this.opts.clipboardUpload) - { - // append hidden fields - postData = this.pasteClipboardAppendFields(postData); + this.keyup.current = this.selection.getCurrent(); + this.keyup.parent = this.selection.getParent(); + var $parent = this.utils.isRedactorParent($(this.keyup.parent).parent()); - $.post(this.opts.clipboardUploadUrl, postData, - $.proxy(function(data) - { - var json = (typeof data === 'string' ? $.parseJSON(data) : data); - var html = '<img src="' + json.filelink + '" id="clipboard-image-marker" />'; - this.execCommand('inserthtml', html, false); + // callback + var keyupStop = this.core.setCallback('keyup', e); + if (keyupStop === false) + { + e.preventDefault(); + return false; + } - var image = $(this.$editor.find('img#clipboard-image-marker')); + // 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')) + { + this.keyup.replaceToParagraph(); + } - if (image.length) image.removeAttr('id'); - else image = false; + // replace div after lists + if (!this.opts.linebreaks && this.utils.isRedactorParent(this.keyup.current) && this.keyup.current.tagName === 'DIV') + { + this.keyup.replaceToParagraph(false); + } - this.sync(); - // upload callback - if (image) + if (!this.opts.linebreaks && $(this.keyup.parent).hasClass('redactor-invisible-space') && ($parent === false || $parent[0].tagName == 'BODY')) { - this.callback('imageUpload', image, json); + $(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); - }, this)); - } - else - { - this.insertHtml('<img src="' + result + '" />'); - } - }, + this.observe.load(); + this.code.sync(); + } - // BUFFER - bufferSet: function(selectionSave) - { - if (selectionSave !== false) - { - this.selectionSave(); - } + if (key === this.keyCode.DELETE || key === this.keyCode.BACKSPACE) + { + // clear unverified + this.clean.clearUnverified(); - this.opts.buffer.push(this.$editor.html()); + if (this.observe.image) + { + e.preventDefault(); - if (selectionSave !== false) - { - this.selectionRemoveMarkers('buffer'); - } + this.image.hideResize(); - }, - bufferUndo: function() - { - if (this.opts.buffer.length === 0) - { - this.focusWithSaveScroll(); - return; - } + this.buffer.set(); + this.image.remove(this.observe.image); + this.observe.image = false; - // rebuffer - this.selectionSave(); - this.opts.rebuffer.push(this.$editor.html()); - this.selectionRestore(false, true); + return false; + } - this.$editor.html(this.opts.buffer.pop()); + // remove empty paragraphs + this.$editor.find('p').each($.proxy(this.utils.removeEmpty, this)); - this.selectionRestore(); - setTimeout($.proxy(this.observeStart, this), 100); - }, - bufferRedo: function() - { - if (this.opts.rebuffer.length === 0) - { - this.focusWithSaveScroll(); - return false; - } + // 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).after(this.selection.getMarkerAsHtml()); + this.selection.restore(); + $(this.keyup.current).remove(); + } + } - // buffer - this.selectionSave(); - this.opts.buffer.push(this.$editor.html()); - this.selectionRestore(false, true); + // 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); - this.$editor.html(this.opts.rebuffer.pop()); - this.selectionRestore(true); - setTimeout($.proxy(this.observeStart, this), 4); - }, + var node; + if (clone === false) + { + node = $('<p>').append($current.html()); + } + else + { + node = $('<p>').append($current.clone()); + } - // OBSERVE - observeStart: function() - { - this.observeImages(); + $current.replaceWith(node); + var next = $(node).next(); + if (typeof(next[0]) !== 'undefined' && next[0].tagName == 'BR') + { + next.remove(); + } - if (this.opts.observeLinks) this.observeLinks(); - }, - observeLinks: function() - { - this.$editor.find('a').on('click', $.proxy(this.linkObserver, this)); + this.caret.setEnd(node); + }, + formatEmpty: function(e) + { + var html = $.trim(this.$editor.html()); - this.$editor.on('click.redactor', $.proxy(function(e) - { - this.linkObserverTooltipClose(e); + if (!this.utils.isEmpty(html)) return; - }, this)); + e.preventDefault(); - $(document).on('click.redactor', $.proxy(function(e) - { - this.linkObserverTooltipClose(e); + if (this.opts.linebreaks) + { + this.$editor.html(this.selection.getMarkerAsHtml()); + this.selection.restore(); + } + else + { + html = '<p><br /></p>'; - }, this)); - }, - observeImages: function() - { - if (this.opts.observeImages === false) return false; + this.$editor.html(html); + this.focus.setStart(); + } - this.$editor.find('img').each($.proxy(function(i, elem) - { - if (this.browser('msie')) $(elem).attr('unselectable', 'on'); + this.code.sync(); - var parent = $(elem).parent(); - if (!parent.hasClass('royalSlider') && !parent.hasClass('fotorama')) + return false; + } + }; + }, + lang: function() + { + return { + load: function() { - this.imageResize(elem); + this.opts.curLang = this.opts.langs[this.opts.lang]; + }, + get: function(name) + { + return (typeof this.opts.curLang[name] != 'undefined') ? this.opts.curLang[name] : ''; } - - }, this)); - - // royalSlider and fotorama - this.$editor.find('.fotorama, .royalSlider').on('click', $.proxy(this.editGallery, this)); - + }; }, - linkObserver: function(e) + line: function() { - var $link = $(e.target); + return { + insert: function() + { + this.buffer.set(); - var parent = $(e.target).parent(); - if (parent.hasClass('royalSlider') || parent.hasClass('fotorama')) - { - return; - } + var blocks = this.selection.getBlocks(); + if (blocks[0] !== false && this.line.isExceptLastOrFirst(blocks)) + { + if (!this.utils.browser('msie')) this.$editor.focus(); + return; + } - if ($link.size() == 0 || $link[0].tagName !== 'A') return; + if (this.utils.browser('msie')) + { + this.line.insertInIe(); + } + else + { + this.line.insertInOthersBrowsers(); + } + }, + isExceptLastOrFirst: function(blocks) + { + var exceptTags = ['li', 'td', 'th', 'blockquote', 'figcaption', 'pre', 'dl', 'dt', 'dd']; - var pos = $link.offset(); - if (this.opts.iframe) - { - var posFrame = this.$frame.offset(); - pos.top = posFrame.top + (pos.top - $(this.document).scrollTop()); - pos.left += posFrame.left; - } + var first = blocks[0].tagName.toLowerCase(); + var last = this.selection.getLastBlock(); - var tooltip = $('<span class="redactor-link-tooltip"></span>'); + last = (typeof last == 'undefined') ? first : last.tagName.toLowerCase(); - var href = $link.attr('href'); - if (href === undefined) - { - href = ''; - } + var firstFound = $.inArray(first, exceptTags) != -1; + var lastFound = $.inArray(last, exceptTags) != -1; - if (href.length > 24) href = href.substring(0, 24) + '...'; + if ((firstFound && lastFound) || firstFound) + { + return true; + } + }, + insertInIe: function() + { + this.utils.saveScroll(); + this.buffer.set(); - var aLink = $('<a href="' + $link.attr('href') + '" target="_blank">' + href + '</a>').on('click', $.proxy(function(e) - { - this.linkObserverTooltipClose(false); - }, this)); + this.insert.node(document.createElement('hr')); - var aEdit = $('<a href="#">' + this.opts.curLang.edit + '</a>').on('click', $.proxy(function(e) - { - e.preventDefault(); - this.linkShow(); - this.linkObserverTooltipClose(false); + this.utils.restoreScroll(); + this.code.sync(); + }, + insertInOthersBrowsers: function() + { + this.buffer.set(); - }, this)); + var extra = '<p id="redactor-insert-line"><br /></p>'; + if (this.opts.linebreaks) extra = '<br id="redactor-insert-line">'; - var aUnlink = $('<a href="#">' + this.opts.curLang.unlink + '</a>').on('click', $.proxy(function(e) - { - e.preventDefault(); - this.execCommand('unlink'); - this.linkObserverTooltipClose(false); + document.execCommand('insertHTML', false, '<hr>' + extra); - }, this)); + this.line.setFocus(); + this.code.sync(); + }, + setFocus: function() + { + var node = this.$editor.find('#redactor-insert-line'); + var next = $(node).next()[0]; - - tooltip.append(aLink); - tooltip.append(' | '); - tooltip.append(aEdit); - tooltip.append(' | '); - tooltip.append(aUnlink); - tooltip.css({ - top: (pos.top + 20) + 'px', - left: pos.left + 'px' - }); - - $('.redactor-link-tooltip').remove(); - $('body').append(tooltip); + if (next) + { + this.caret.setAfter(node); + node.remove(); + } + else + { + node.removeAttr('id'); + } + } + }; }, - linkObserverTooltipClose: function(e) + link: function() { - if (e !== false && e.target.tagName == 'A') return false; - $('.redactor-link-tooltip').remove(); - }, - - // SELECTION - getSelection: function() - { - if (!this.opts.rangy) return this.document.getSelection(); - else // rangy - { - if (!this.opts.iframe) return rangy.getSelection(); - else return rangy.getSelection(this.$frame[0]); - } - }, - getRange: function() - { - if (!this.opts.rangy) - { - if (this.document.getSelection) + return { + show: function(e) { - var sel = this.getSelection(); - if (sel.getRangeAt && sel.rangeCount) return sel.getRangeAt(0); - } + if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); - return this.document.createRange(); - } - else // rangy - { - if (!this.opts.iframe) return rangy.createRange(); - else return rangy.createRange(this.iframeDoc()); - } - }, - selectionElement: function(node) - { - this.setCaret(node); - }, - selectionStart: function(node) - { - this.selectionSet(node[0] || node, 0, null, 0); - }, - selectionEnd: function(node) - { - this.selectionSet(node[0] || node, 1, null, 1); - }, - selectionSet: function(orgn, orgo, focn, foco) - { - if (focn == null) focn = orgn; - if (foco == null) foco = orgo; + this.modal.load('link', this.lang.get('link_insert'), 600); - var sel = this.getSelection(); - if (!sel) return; + this.modal.createCancelButton(); + this.link.buttonInsert = this.modal.createActionButton(this.lang.get('insert')); - if (orgn.tagName == 'P' && orgn.innerHTML == '') - { - orgn.innerHTML = this.opts.invisibleSpace; - } + this.selection.get(); - if (orgn.tagName == 'BR' && this.opts.linebreaks === false) - { - var par = $(this.opts.emptyHtml)[0]; - $(orgn).replaceWith(par); - orgn = par; - focn = orgn; - } + this.link.getData(); + this.link.cleanUrl(); - var range = this.getRange(); - range.setStart(orgn, orgo); - range.setEnd(focn, foco ); + if (this.link.target == '_blank') $('#redactor-link-blank').prop('checked', true); - try { - sel.removeAllRanges(); - } catch (e) {} + this.link.$inputUrl = $('#redactor-link-url'); + this.link.$inputText = $('#redactor-link-url-text'); - sel.addRange(range); - }, - selectionWrap: function(tag) - { - tag = tag.toLowerCase(); + this.link.$inputText.val(this.link.text); + this.link.$inputUrl.val(this.link.url); - var block = this.getBlock(); - if (block) - { - var wrapper = this.formatChangeTag(block, tag); - this.sync(); - return wrapper; - } + this.link.buttonInsert.on('click', $.proxy(this.link.insert, this)); - var sel = this.getSelection(); - var range = sel.getRangeAt(0); - var wrapper = document.createElement(tag); - wrapper.appendChild(range.extractContents()); - range.insertNode(wrapper); + // hide link's tooltip + $('.redactor-link-tooltip').remove(); - this.selectionElement(wrapper); + // show modal + this.selection.save(); + this.modal.show(); + this.link.$inputUrl.focus(); + }, + cleanUrl: function() + { + var thref = self.location.href.replace(/\/$/i, ''); + this.link.url = this.link.url.replace(thref, ''); + this.link.url = this.link.url.replace(/^\/#/, '#'); + this.link.url = this.link.url.replace('mailto:', ''); - return wrapper; - }, - selectionAll: function() - { - var range = this.getRange(); - range.selectNodeContents(this.$editor[0]); + // 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, ''); + } - var sel = this.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - }, - selectionRemove: function() - { - this.getSelection().removeAllRanges(); - }, - getCaretOffset: function (element) - { - var caretOffset = 0; + }, + getData: function() + { + this.link.$node = false; - var range = this.getRange(); - var preCaretRange = range.cloneRange(); - preCaretRange.selectNodeContents(element); - preCaretRange.setEnd(range.endContainer, range.endOffset); - caretOffset = $.trim(preCaretRange.toString()).length; + var $el = $(this.selection.getCurrent()).closest('a'); + if ($el.size() !== 0 && $el[0].tagName === 'A') + { + this.link.$node = $el; - return caretOffset; - }, - getCaretOffsetRange: function() - { - return new Range(this.getSelection().getRangeAt(0)); - }, - setCaret: function (el, start, end) - { - if (typeof end === 'undefined') end = start; - el = el[0] || el; + this.link.url = $el.attr('href'); + this.link.text = $el.text(); + this.link.target = $el.attr('target'); + } + else + { + this.link.text = this.sel.toString(); + this.link.url = ''; + this.link.target = ''; + } - var range = this.getRange(); - range.selectNodeContents(el); - - var textNodes = this.getTextNodesIn(el); - var foundStart = false; - var charCount = 0, endCharCount; - - if (textNodes.length == 1 && start) - { - range.setStart(textNodes[0], start); - range.setEnd(textNodes[0], end); - } - else - { - for (var i = 0, textNode; textNode = textNodes[i++];) + }, + insert: function() { - endCharCount = charCount + textNode.length; - if (!foundStart && start >= charCount && (start < endCharCount || (start == endCharCount && i < textNodes.length))) + var target = ''; + var link = this.link.$inputUrl.val(); + var text = this.link.$inputText.val(); + + if ($.trim(link) === '') { - range.setStart(textNode, start - charCount); - foundStart = true; + this.link.$inputUrl.addClass('redactor-input-error').on('keyup', function() + { + $(this).removeClass('redactor-input-error'); + $(this).off('keyup'); + + }); + + return; } - if (foundStart && end <= endCharCount) + // mailto + if (link.search('@') != -1 && /(http|ftp|https):\/\//i.test(link) === false) { - range.setEnd( textNode, end - charCount ); - break; + link = 'mailto:' + link; } + // url, not anchor + else if (link.search('#') !== 0) + { + if ($('#redactor-link-blank').prop('checked')) + { + target = '_blank'; + } - charCount = endCharCount; - } - } + // 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'); - var sel = this.getSelection(); - sel.removeAllRanges(); - sel.addRange( range ); - }, - setCaretAfter: function(node) - { - this.$editor.focus(); + if (link.search(re) == -1 && link.search(re2) === 0 && this.opts.linkProtocol) + { + link = this.opts.linkProtocol + '://' + link; + } + } - node = node[0] || node; + this.link.set(text, link, target); + this.modal.close(); + }, + set: function(text, link, target) + { + text = $.trim(text.replace(/<|>/g, '')); - var range = this.document.createRange() + this.selection.restore(); - var start = 1; - var end = -1; + if (text === '' && link === '') return; + if (text === '' && link !== '') text = link; - range.setStart(node, start) - range.setEnd(node, end + 2) + if (this.link.$node) + { + this.buffer.set(); + this.link.$node.text(text).attr('href', link); + if (target !== '') + { + this.link.$node.attr('target', target); + } + else + { + this.link.$node.removeAttr('target'); + } - var selection = this.window.getSelection() - var cursorRange = this.document.createRange() + 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); - var emptyElement = this.document.createTextNode('\u200B') - $(node).after(emptyElement) + this.insert.node($a); + this.selection.selectElement($a); + } + else + { + var $a; + if (this.utils.browser('msie')) + { + $a = $('<a href="' + link + '">').text(text); + if (target !== '') $a.attr('target', target); - cursorRange.setStartAfter(emptyElement) + $a = $(this.insert.node($a)); + this.selection.selectElement($a); + } + else + { + document.execCommand('createLink', false, link); - selection.removeAllRanges() - selection.addRange(cursorRange) - $(emptyElement).remove(); - }, - getTextNodesIn: function (node) - { - var textNodes = []; + $a = $(this.selection.getCurrent()).closest('a'); - if (node.nodeType == 3) textNodes.push(node); - else - { - var children = node.childNodes; - for (var i = 0, len = children.length; i < len; ++i) - { - textNodes.push.apply(textNodes, this.getTextNodesIn(children[i])); - } - } + if (target !== '') $a.attr('target', target); + $a.removeAttr('style'); - return textNodes; - }, + if (this.link.text === '') + { + $a.text(text); + this.selection.selectElement($a); + } + } + } - // GET ELEMENTS - getCurrent: function() - { - var el = false; - var sel = this.getSelection(); + this.code.sync(); + this.core.setCallback('insertedLink', $a); - if (sel && sel.rangeCount > 0) - { - el = sel.getRangeAt(0).startContainer; - //el = sel.getRangeAt(0).commonAncestorContainer; - } + } - return this.isParentRedactor(el); - }, - getParent: function(elem) - { - elem = elem || this.getCurrent(); - if (elem) return this.isParentRedactor( $( elem ).parent()[0] ); - else return false; - }, - getBlock: function(node) - { - if (typeof node === 'undefined') node = this.getCurrent(); + // link tooltip + setTimeout($.proxy(function() + { + this.observe.links(); - while (node) - { - if (this.nodeTestBlocks(node)) + }, this), 5); + }, + unlink: function(e) { - if ($(node).hasClass('redactor_editor')) return false; - return node; - } + if (typeof e != 'undefined' && e.preventDefault) e.preventDefault(); - node = node.parentNode; - } + var nodes = this.selection.getNodes(); + if (!nodes) return; - return false; - }, - getBlocks: function(nodes) - { - var newnodes = []; - if (typeof nodes == 'undefined') - { - var range = this.getRange(); - if (range && range.collapsed === true) return [this.getBlock()]; - var nodes = this.getNodes(range); - } + this.buffer.set(); - $.each(nodes, $.proxy(function(i,node) - { - if (this.opts.iframe === false && $(node).parents('div.redactor_editor').size() == 0) return false; - if (this.nodeTestBlocks(node)) newnodes.push(node); + var len = nodes.length; + for (var i = 0; i < len; i++) + { + if (nodes[i].tagName == 'A') + { + var $node = $(nodes[i]); + $node.replaceWith($node.contents()); + } + } - }, this)); + // hide link's tooltip + $('.redactor-link-tooltip').remove(); - if (newnodes.length === 0) newnodes = [this.getBlock()]; + this.code.sync(); - return newnodes; + } + }; }, - isInlineNode: function(node) + list: function() { - if (node.nodeType != 1) return false; - - return !this.rTestBlock.test(node.nodeName); - }, - nodeTestBlocks: function(node) - { - return node.nodeType == 1 && this.rTestBlock.test(node.nodeName); - }, - tagTestBlock: function(tag) - { - return this.rTestBlock.test(tag); - }, - getNodes: function(range, tag) - { - if (typeof range == 'undefined' || range == false) var range = this.getRange(); - if (range && range.collapsed === true) - { - if (typeof tag === 'undefined' && this.tagTestBlock(tag)) + return { + toggle: function(cmd) { - var block = this.getBlock(); - if (block.tagName == tag) return [block]; - else return []; - } - else - { - return [this.getCurrent()]; - } - } + this.placeholder.remove(); + if (!this.utils.browser('msie')) this.$editor.focus(); - var nodes = [], finalnodes = []; + this.buffer.set(); + this.selection.save(); - var sel = this.document.getSelection(); - if (!sel.isCollapsed) nodes = this.getRangeSelectedNodes(sel.getRangeAt(0)); + var parent = this.selection.getParent(); + var $list = $(parent).closest('ol, ul'); - $.each(nodes, $.proxy(function(i,node) - { - if (this.opts.iframe === false && $(node).parents('div.redactor_editor').size() == 0) return false; + if (!this.utils.isRedactorParent($list) && $list.size() !== 0) + { + $list = false; + } - if (typeof tag === 'undefined') - { - if ($.trim(node.textContent) != '') + var isUnorderedCmdOrdered, isOrderedCmdUnordered; + var remove = false; + if ($list && $list.length) { - finalnodes.push(node); + remove = true; + var listTag = $list[0].tagName; + + isUnorderedCmdOrdered = (cmd === 'orderedlist' && listTag === 'UL'); + isOrderedCmdUnordered = (cmd === 'unorderedlist' && listTag === 'OL'); } - } - else if (node.tagName == tag) - { - finalnodes.push(node); - } - }, this)); + if (isUnorderedCmdOrdered) + { + this.utils.replaceToTag($list, 'ol'); + } + else if (isOrderedCmdUnordered) + { + this.utils.replaceToTag($list, 'ul'); + } + else + { + if (remove) + { + this.list.remove(cmd); + } + else + { + this.list.insert(cmd); + } + } - if (finalnodes.length == 0) - { - if (typeof tag === 'undefined' && this.tagTestBlock(tag)) + + this.selection.restore(); + this.code.sync(); + }, + insert: function(cmd) { - var block = this.getBlock(); - if (block.tagName == tag) return finalnodes.push(block); - else return []; - } - else - { - finalnodes.push(this.getCurrent()); - } - } + var parent = this.selection.getParent(); + var current = this.selection.getCurrent(); + var $td = $(current).closest('td, th'); - // last element filtering - var last = finalnodes[finalnodes.length-1]; - if (this.nodeTestBlocks(last)) - { - finalnodes = finalnodes.slice(0, -1); - } + if (this.utils.browser('msie') && this.opts.linebreaks) + { + this.list.insertInIe(cmd); + } + else + { + document.execCommand('insert' + cmd); + } - return finalnodes; - }, - getElement: function(node) - { - if (!node) node = this.getCurrent(); - while (node) - { - if (node.nodeType == 1) - { - if ($(node).hasClass('redactor_editor')) return false; - return node; - } + var $list = $(this.selection.getParent()).closest('ol, ul'); - node = node.parentNode; - } + if ($td.size() !== 0) + { + var prev = $td.prev(); + var html = $td.html(); + $td.html(''); + if (prev && prev.length === 1 && (prev[0].tagName === 'TD' || prev[0].tagName === 'TH')) + { + $(prev).after($td); + } + else + { + $(parent).prepend($td); + } - return false; - }, - getRangeSelectedNodes: function(range) - { - range = range || this.getRange(); - var node = range.startContainer; - var endNode = range.endContainer; + $td.html(html); + } - if (node == endNode) return [node]; + if (this.utils.isEmpty($list.find('li').text())) + { + var $children = $list.children('li'); + $children.find('br').remove(); + $children.append(this.selection.getMarkerAsHtml()); + } - var rangeNodes = []; - while (node && node != endNode) - { - rangeNodes.push(node = this.nextNode(node)); - } + if ($list.length) + { + // remove block-element list wrapper + var $listParent = $list.parent(); + if (this.utils.isRedactorParent($listParent) && $listParent[0].tagName != 'LI' && this.utils.isBlock($listParent[0])) + { + $listParent.replaceWith($listParent.contents()); + } + } - node = range.startContainer; - while (node && node != range.commonAncestorContainer) - { - rangeNodes.unshift(node); - node = node.parentNode; - } + if (!this.utils.browser('msie')) + { + this.$editor.focus(); + } - return rangeNodes; - }, - nextNode: function(node) - { - if (node.hasChildNodes()) return node.firstChild; - else - { - while (node && !node.nextSibling) + this.clean.clearUnverified(); + }, + insertInIe: function(cmd) { - node = node.parentNode; - } + var wrapper = this.selection.wrap('div'); + var wrapperHtml = $(wrapper).html(); - if (!node) return null; - return node.nextSibling; - } - }, + var tmpList = (cmd == 'orderedlist') ? $('<ol>') : $('<ul>'); + var tmpLi = $('<li>'); - // GET SELECTION HTML OR TEXT - getSelectionText: function() - { - return this.getSelection().toString(); - }, - getSelectionHtml: function() - { - var html = ''; + if ($.trim(wrapperHtml) === '') + { + tmpLi.append(this.selection.getMarkerAsHtml()); + tmpList.append(tmpLi); + this.$editor.find('#selection-marker-1').replaceWith(tmpList); + } + else + { + var items = wrapperHtml.split(/<br\s?\/?>/gi); + if (items) + { + for (var i = 0; i < items.length; i++) + { + if ($.trim(items[i]) !== '') + { + tmpList.append($('<li>').html(items[i])); + } + } + } + else + { + tmpLi.append(wrapperHtml); + tmpList.append(tmpLi); + } - var sel = this.getSelection(); - if (sel.rangeCount) - { - var container = this.document.createElement( "div" ); - var len = sel.rangeCount; - for (var i = 0; i < len; ++i) + $(wrapper).replaceWith(tmpList); + } + }, + remove: function(cmd) { - container.appendChild(sel.getRangeAt(i).cloneContents()); - } + document.execCommand('insert' + cmd); - html = container.innerHTML; - } + var $current = $(this.selection.getCurrent()); - return this.syncClean(html); - }, + this.indent.fixEmptyIndent(); - // SAVE & RESTORE - selectionSave: function() - { - if (!this.isFocused()) - { - this.focusWithSaveScroll(); - } + if (!this.opts.linebreaks && $current.closest('li, th, td').size() === 0) + { + document.execCommand('formatblock', false, 'p'); + this.$editor.find('ul, ol, blockquote').each($.proxy(this.utils.removeEmpty, this)); + } - if (!this.opts.rangy) - { - this.selectionCreateMarker(this.getRange()); - } - // rangy - else - { - this.savedSel = rangy.saveSelection(); - } + var $table = $(this.selection.getCurrent()).closest('table'); + var $prev = $table.prev(); + if (!this.opts.linebreaks && $table.size() !== 0 && $prev.size() !== 0 && $prev[0].tagName == 'BR') + { + $prev.remove(); + } + + this.clean.clearUnverified(); + + } + }; }, - selectionCreateMarker: function(range, remove) + modal: function() { - if (!range) return; + return { + callbacks: {}, + loadTemplates: function() + { + this.opts.modal = { + imageEdit: String() + + '<section id="redactor-modal-image-edit">' + + '<label>' + this.lang.get('title') + '</label>' + + '<input type="text" id="redactor-image-title" />' + + '<label class="redactor-image-link-option">' + this.lang.get('link') + '</label>' + + '<input type="text" id="redactor-image-link" class="redactor-image-link-option" />' + + '<label class="redactor-image-link-option"><input type="checkbox" id="redactor-image-link-blank"> ' + this.lang.get('link_new_tab') + '</label>' + + '<label class="redactor-image-position-option">' + this.lang.get('image_position') + '</label>' + + '<select class="redactor-image-position-option" id="redactor-image-align">' + + '<option value="none">' + this.lang.get('none') + '</option>' + + '<option value="left">' + this.lang.get('left') + '</option>' + + '<option value="center">' + this.lang.get('center') + '</option>' + + '<option value="right">' + this.lang.get('right') + '</option>' + + '</select>' + + '</section>', - var node1 = $('<span id="selection-marker-1" class="redactor-selection-marker">' + this.opts.invisibleSpace + '</span>', this.document)[0]; - var node2 = $('<span id="selection-marker-2" class="redactor-selection-marker">' + this.opts.invisibleSpace + '</span>', this.document)[0]; + image: String() + + '<section id="redactor-modal-image-insert">' + + '<div id="redactor-modal-image-droparea"></div>' + + '</section>', - if (range.collapsed === true) - { - this.selectionSetMarker(range, node1, true); - } - else - { - this.selectionSetMarker(range, node1, true); - this.selectionSetMarker(range, node2, false); - } + file: String() + + '<section id="redactor-modal-file-insert">' + + '<div id="redactor-modal-file-upload-box">' + + '<label>' + this.lang.get('filename') + '</label>' + + '<input type="text" id="redactor-filename" /><br><br>' + + '<div id="redactor-modal-file-upload"></div>' + + '</div>' + + '</section>', - this.savedSel = this.$editor.html(); + link: String() + + '<section id="redactor-modal-link-insert">' + + '<label>URL</label>' + + '<input type="url" id="redactor-link-url" />' + + '<label>' + this.lang.get('text') + '</label>' + + '<input type="text" id="redactor-link-url-text" />' + + '<label><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get('link_new_tab') + '</label>' + + '</section>' + }; - this.selectionRestore(false, false); - }, - selectionSetMarker: function(range, node, type) - { - var boundaryRange = range.cloneRange(); - try { - boundaryRange.collapse(type); - boundaryRange.insertNode(node); - boundaryRange.detach(); - } - catch (e) - { - var html = this.opts.emptyHtml; - if (this.opts.linebreaks) html = '<br>'; + $.extend(this.opts, this.opts.modal); - this.$editor.prepend(html); - this.focus(); - } - }, - selectionRestore: function(replace, remove) - { - if (!this.opts.rangy) - { - if (replace === true && this.savedSel) + }, + addCallback: function(name, callback) { - this.$editor.html(this.savedSel); - } + this.modal.callbacks[name] = callback; + }, + createTabber: function($modal) + { + this.modal.$tabber = $('<div>').attr('id', 'redactor-modal-tabber'); - var node1 = this.$editor.find('span#selection-marker-1'); - var node2 = this.$editor.find('span#selection-marker-2'); + $modal.prepend(this.modal.$tabber); + }, + addTab: function(id, name, active) + { + var $tab = $('<a href="#" rel="tab' + id + '">').text(name); + if (active) + { + $tab.addClass('active'); + } - if (this.browser('mozilla')) + var self = this; + $tab.on('click', function(e) + { + e.preventDefault(); + $('.redactor-tab').hide(); + $('.redactor-' + $(this).attr('rel')).show(); + + self.modal.$tabber.find('a').removeClass('active'); + $(this).addClass('active'); + + }); + + this.modal.$tabber.append($tab); + }, + addTemplate: function(name, template) { - this.$editor.focus(); - } - else if (!this.isFocused()) + this.opts.modal[name] = template; + }, + getTemplate: function(name) { - this.focusWithSaveScroll(); - } - - if (node1.length != 0 && node2.length != 0) + return this.opts.modal[name]; + }, + getModal: function() { - - this.selectionSet(node1[0], 0, node2[0], 0); - } - else if (node1.length != 0) + return this.$modalBody.find('section'); + }, + load: function(templateName, title, width) { - this.selectionSet(node1[0], 0, null, 0); - } + this.modal.templateName = templateName; + this.modal.width = width; - if (remove !== false) + this.modal.build(); + this.modal.enableEvents(); + this.modal.setTitle(title); + this.modal.setDraggable(); + this.modal.setContent(); + + // callbacks + if (typeof this.modal.callbacks[templateName] != 'undefined') + { + this.modal.callbacks[templateName].call(this); + } + + }, + show: function() { - this.selectionRemoveMarkers(); - this.savedSel = false; - } - } - // rangy - else - { - rangy.restoreSelection(this.savedSel); - } - }, - selectionRemoveMarkers: function(type) - { - if (!this.opts.rangy) - { - $.each(this.$editor.find('span.redactor-selection-marker'), function() - { - var html = $.trim($(this).html().replace(/[^\u0000-\u1C7F]/g, '')); - if (html == '') + // ios keyboard hide + if (this.utils.isMobile() && !this.utils.browser('msie')) { - $(this).remove(); + document.activeElement.blur(); } + + $(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).removeAttr('class').removeAttr('id'); + this.modal.showOnDesktop(); } - }); - } - // rangy - else - { - rangy.removeMarkers(this.savedSel); - } - }, - // TABLE - tableShow: function() - { - this.selectionSave(); + this.$modalOverlay.show(); + this.$modalBox.show(); - this.modalInit(this.opts.curLang.table, this.opts.modal_table, 300, $.proxy(function() - { - $('#redactor_insert_table_btn').click($.proxy(this.tableInsert, this)); + this.modal.setButtonsWidth(); - setTimeout(function() - { - $('#redactor_table_rows').focus(); + this.utils.saveScroll(); - }, 200); + // resize + if (!this.utils.isMobile()) + { + setTimeout($.proxy(this.modal.showOnDesktop, this), 0); + $(window).on('resize.redactor-modal', $.proxy(this.modal.resize, this)); + } - }, this)); - }, - tableInsert: function() - { - this.bufferSet(false); + // modal shown callback + this.core.setCallback('modalOpened', this.modal.templateName, this.$modal); - var rows = $('#redactor_table_rows').val(), - columns = $('#redactor_table_columns').val(), - $table_box = $('<div></div>'), - tableId = Math.floor(Math.random() * 99999), - $table = $('<table id="table' + tableId + '"><tbody></tbody></table>'), - i, $row, z, $column; + // fix bootstrap modal focus + $(document).off('focusin.modal'); - for (i = 0; i < rows; i++) - { - $row = $('<tr></tr>'); + // enter + this.$modal.find('input[type=text]').on('keydown.redactor-modal', $.proxy(this.modal.setEnter, this)); - for (z = 0; z < columns; z++) + }, + showOnDesktop: function() { - $column = $('<td>' + this.opts.invisibleSpace + '</td>'); + var height = this.$modal.outerHeight(); + var windowHeight = $(window).height(); + var windowWidth = $(window).width(); - // set the focus to the first td - if (i === 0 && z === 0) + if (this.modal.width > windowWidth) { - $column.append('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>'); + this.$modal.css({ + width: '96%', + marginTop: (windowHeight/2 - height/2) + 'px' + }); + return; } - $($row).append($column); - } + if (height > windowHeight) + { + this.$modal.css({ + width: this.modal.width + 'px', + marginTop: '20px' + }); + } + else + { + this.$modal.css({ + width: this.modal.width + 'px', + marginTop: (windowHeight/2 - height/2) + 'px' + }); + } + }, + showOnMobile: function() + { + this.$modal.css({ + width: '96%', + marginTop: '2%' + }); - $table.append($row); - } + }, + resize: function() + { + if (this.utils.isMobile()) + { + this.modal.showOnMobile(); + } + else + { + this.modal.showOnDesktop(); + } + }, + setTitle: function(title) + { + this.$modalHeader.html(title); + }, + setContent: function() + { + this.$modalBody.html(this.modal.getTemplate(this.modal.templateName)); + }, + setDraggable: function() + { + if (typeof $.fn.draggable === 'undefined') return; - $table_box.append($table); - var html = $table_box.html(); + this.$modal.draggable({ handle: this.$modalHeader }); + this.$modalHeader.css('cursor', 'move'); + }, + setEnter: function(e) + { + if (e.which != 13) return; - if (this.opts.linebreaks === false && this.browser('mozilla')) - { - html += '<p>' + this.opts.invisibleSpace + '</p>'; - } + e.preventDefault(); + this.$modal.find('button.redactor-modal-action-btn').click(); + }, + createCancelButton: function() + { + var button = $('<button>').addClass('redactor-modal-btn redactor-modal-close-btn').html(this.lang.get('cancel')); + button.on('click', $.proxy(this.modal.close, this)); - this.modalClose(); - this.selectionRestore(); + this.$modalFooter.append(button); + }, + createDeleteButton: function(label) + { + return this.modal.createButton(label, 'delete'); + }, + createActionButton: function(label) + { + return this.modal.createButton(label, 'action'); + }, + createButton: function(label, className) + { + var button = $('<button>').addClass('redactor-modal-btn').addClass('redactor-modal-' + className + '-btn').html(label); + this.$modalFooter.append(button); - var current = this.getBlock() || this.getCurrent(); + return button; + }, + setButtonsWidth: function() + { + var buttons = this.$modalFooter.find('button'); + var buttonsSize = buttons.size(); + if (buttonsSize === 0) return; - if (current && current.tagName != 'BODY') - { - if (current.tagName == 'LI') + buttons.css('width', (100/buttonsSize) + '%'); + }, + build: function() { - var current = $(current).closest('ul, ol'); - } + this.modal.buildOverlay(); - $(current).after(html) - } - else - { + this.$modalBox = $('<div id="redactor-modal-box" />').hide(); + this.$modal = $('<div id="redactor-modal" />'); + this.$modalHeader = $('<header />'); + this.$modalClose = $('<span id="redactor-modal-close" />').html('&times;'); + this.$modalBody = $('<div id="redactor-modal-body" />'); + this.$modalFooter = $('<footer />'); - this.insertHtmlAdvanced(html, false); - } + this.$modal.append(this.$modalHeader); + this.$modal.append(this.$modalClose); + this.$modal.append(this.$modalBody); + this.$modal.append(this.$modalFooter); + this.$modalBox.append(this.$modal); + this.$modalBox.appendTo(document.body); + }, + buildOverlay: function() + { + this.$modalOverlay = $('<div id="redactor-modal-overlay">').hide(); + $('body').prepend(this.$modalOverlay); + }, + enableEvents: function() + { + this.$modalClose.on('click.redactor-modal', $.proxy(this.modal.close, this)); + $(document).on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this)); + this.$editor.on('keyup.redactor-modal', $.proxy(this.modal.closeHandler, this)); + this.$modalBox.on('click.redactor-modal', $.proxy(this.modal.close, this)); + }, + disableEvents: function() + { + this.$modalClose.off('click.redactor-modal'); + $(document).off('keyup.redactor-modal'); + this.$editor.off('keyup.redactor-modal'); + this.$modalBox.off('click.redactor-modal'); + $(window).off('resize.redactor-modal'); + }, + closeHandler: function(e) + { + if (e.which != this.keyCode.ESC) return; - this.selectionRestore(); + this.modal.close(false); + }, + close: function(e) + { + if (e) + { + if (!$(e.target).hasClass('redactor-modal-close-btn') && e.target != this.$modalClose[0] && e.target != this.$modalBox[0]) + { + return; + } - var table = this.$editor.find('#table' + tableId); - this.buttonActiveObserver(); + e.preventDefault(); + } - table.find('span#selection-marker-1, inline#selection-marker-1').remove(); - table.removeAttr('id'); + if (!this.$modalBox) return; - this.sync(); - }, - tableDeleteTable: function() - { - var $table = $(this.getParent()).closest('table'); - if (!this.isParentRedactor($table)) return false; - if ($table.size() == 0) return false; + this.modal.disableEvents(); - this.bufferSet(); + this.$modalOverlay.remove(); - $table.remove(); - this.sync(); - }, - tableDeleteRow: function() - { - var parent = this.getParent(); - var $table = $(parent).closest('table'); + this.$modalBox.fadeOut('fast', $.proxy(function() + { + this.$modalBox.remove(); + setTimeout($.proxy(this.utils.restoreScroll, this), 0); - if (!this.isParentRedactor($table)) return false; - if ($table.size() == 0) return false; + if (e !== undefined) this.selection.restore(); - this.bufferSet(); + $(document.body).css('overflow', this.modal.bodyOveflow); + this.core.setCallback('modalClosed', this.modal.templateName); - var $current_tr = $(parent).closest('tr'); - var $focus_tr = $current_tr.prev().length ? $current_tr.prev() : $current_tr.next(); - if ($focus_tr.length) - { - var $focus_td = $focus_tr.children('td' ).first(); - if ($focus_td.length) - { - $focus_td.prepend('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>'); - } - } + }, this)); - $current_tr.remove(); - this.selectionRestore(); - $table.find('span#selection-marker-1').remove(); - this.sync(); + } + }; }, - tableDeleteColumn: function() + observe: function() { - var parent = this.getParent(); - var $table = $(parent).closest('table'); + return { + load: function() + { + this.observe.images(); + this.observe.links(); + }, + buttons: function(e, btnName) + { + var current = this.selection.getCurrent(); + var parent = this.selection.getParent(); - if (!this.isParentRedactor($table)) return false; - if ($table.size() == 0) return false; + this.button.setInactiveAll(btnName); - this.bufferSet(); + if (e === false && btnName !== 'html') + { + if ($.inArray(btnName, this.opts.activeButtons) != -1) this.button.toggleActive(btnName); + return; + } - var $current_td = $(parent).closest('td'); - if (!($current_td.is('td'))) - { - $current_td = $current_td.closest('td'); - } + //var linkButtonName = (this.utils.isCurrentOrParent('A')) ? this.lang.get('link_edit') : this.lang.get('link_insert'); + //$('body').find('a.redactor-dropdown-link').text(linkButtonName); - var index = $current_td.get(0).cellIndex; + $.each(this.opts.activeButtonsStates, $.proxy(function(key, value) + { + var parentEl = $(parent).closest(key); + var currentEl = $(current).closest(key); - // Set the focus correctly - $table.find('tr').each($.proxy(function(i, elem) - { - var focusIndex = index - 1 < 0 ? index + 1 : index - 1; - if (i === 0) - { - $(elem).find('td').eq(focusIndex).prepend('<span id="selection-marker-1">' + this.opts.invisibleSpace + '</span>'); - } + if (parentEl.length !== 0 && !this.utils.isRedactorParent(parentEl)) return; + if (!this.utils.isRedactorParent(currentEl)) return; + if (parentEl.length !== 0 || currentEl.closest(key).length !== 0) + { + this.button.setActive(value); + } - $(elem).find('td').eq(index).remove(); + }, this)); - }, this)); + var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase()); + if (this.utils.isRedactorParent(parent) && $parent.length) + { + var align = ($parent.css('text-align') === '') ? 'left' : $parent.css('text-align'); + this.button.setActive('align' + align); + } + }, + addButton: function(tag, btnName) + { + this.opts.activeButtons.push(btnName); + this.opts.activeButtonsStates[tag] = btnName; + }, + images: function() + { + this.$editor.find('img').each($.proxy(function(i, img) + { + var $img = $(img); - this.selectionRestore(); - $table.find('span#selection-marker-1').remove(); - this.sync(); - }, - tableAddHead: function() - { - var $table = $(this.getParent()).closest('table'); - if (!this.isParentRedactor($table)) return false; - if ($table.size() == 0) return false; + // 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(); }); - this.bufferSet(); + if (this.utils.browser('msie')) $img.attr('unselectable', 'on'); - if ($table.find('thead').size() !== 0) this.tableDeleteHead(); - else - { - var tr = $table.find('tr').first().clone(); - tr.find('td').html(this.opts.invisibleSpace); - $thead = $('<thead></thead>'); - $thead.append(tr); - $table.prepend($thead); + this.image.setEditable($img); - this.sync(); - } - }, - tableDeleteHead: function() - { - var $table = $(this.getParent()).closest('table'); - if (!this.isParentRedactor($table)) return false; - var $thead = $table.find('thead'); + }, this)); - if ($thead.size() == 0) return false; + $(document).on('click.redactor-image-delete', $.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; + } - this.bufferSet(); + }, this)); - $thead.remove(); - this.sync(); - }, - tableAddRowAbove: function() - { - this.tableAddRow('before'); - }, - tableAddRowBelow: function() - { - this.tableAddRow('after'); - }, - tableAddColumnLeft: function() - { - this.tableAddColumn('before'); - }, - tableAddColumnRight: function() - { - this.tableAddColumn('after'); - }, - tableAddRow: function(type) - { - var $table = $(this.getParent()).closest('table'); - if (!this.isParentRedactor($table)) return false; - if ($table.size() == 0) return false; + }, + links: function() + { + if (!this.opts.linkTooltip) return; - this.bufferSet(); + this.$editor.find('a').on('touchstart click', $.proxy(this.observe.showTooltip, this)); + this.$editor.on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this)); + $(document).on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this)); + }, + getTooltipPosition: function($link) + { + return $link.offset(); + }, + showTooltip: function(e) + { + var $link = $(e.target); + var $parent = $link.closest('a'); + var tag = ($link.size() !== 0) ? $link[0].tagName : false; - var $current_tr = $(this.getParent()).closest('tr'); - var new_tr = $current_tr.clone(); - new_tr.find('td').html(this.opts.invisibleSpace); + if ($parent[0].tagName === 'A') + { + if (tag === 'IMG') return; + else if (tag !== 'A') $link = $parent; + } - if (type === 'after') $current_tr.after(new_tr); - else $current_tr.before(new_tr); + if (tag !== 'A') + { + return; + } - this.sync(); - }, - tableAddColumn: function (type) - { - var parent = this.getParent(); - var $table = $(parent).closest('table'); + var pos = this.observe.getTooltipPosition($link); + var tooltip = $('<span class="redactor-link-tooltip"></span>'); - if (!this.isParentRedactor($table)) return false; - if ($table.size() == 0) return false; + var href = $link.attr('href'); + if (href === undefined) + { + href = ''; + } - this.bufferSet(); + if (href.length > 24) href = href.substring(0, 24) + '...'; - var index = 0; + var aLink = $('<a href="' + $link.attr('href') + '" target="_blank" />').html(href).addClass('redactor-link-tooltip-action'); + var aEdit = $('<a href="#" />').html(this.lang.get('edit')).on('click', $.proxy(this.link.show, this)).addClass('redactor-link-tooltip-action'); + var aUnlink = $('<a href="#" />').html(this.lang.get('unlink')).on('click', $.proxy(this.link.unlink, this)).addClass('redactor-link-tooltip-action'); - var current = this.getCurrent(); - var $current_tr = $(current).closest('tr'); - var $current_td = $(current).closest('td'); + tooltip.append(aLink).append(' | ').append(aEdit).append(' | ').append(aUnlink); + tooltip.css({ + top: (pos.top + 20) + 'px', + left: pos.left + 'px' + }); - $current_tr.find('td').each($.proxy(function(i, elem) - { - if ($(elem)[0] === $current_td[0]) index = i; + $('.redactor-link-tooltip').remove(); + $('body').append(tooltip); + }, + closeTooltip: function(e) + { + e = e.originalEvent || e; - }, this)); + var target = e.target; + var $parent = $(target).closest('a'); + if ($parent.size() !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A') + { + return; + } + else if ((target.tagName === 'A' && this.utils.isRedactorParent(target)) || $(target).hasClass('redactor-link-tooltip-action')) + { + return; + } - $table.find('tr').each($.proxy(function(i, elem) - { - var $current = $(elem).find('td').eq(index); + $('.redactor-link-tooltip').remove(); + } - var td = $current.clone(); - td.html(this.opts.invisibleSpace); + }; + }, + paragraphize: function() + { + return { + load: function(html) + { + if (this.opts.linebreaks) return html; + if (html === '' || html === '<p></p>') return this.opts.emptyHtml; - type === 'after' ? $current.after(td) : $current.before(td); + this.paragraphize.blocks = ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption', + 'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea', + 'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p']; - }, this)); + html = html + "\n"; - this.sync(); - }, + this.paragraphize.safes = []; + this.paragraphize.z = 0; - // VIDEO - videoShow: function() - { - this.selectionSave(); + html = html.replace(/(<br\s?\/?>){1,}\n?<\/blockquote>/gi, '</blockquote>'); - this.modalInit(this.opts.curLang.video, this.opts.modal_video, 600, $.proxy(function() - { - $('#redactor_insert_video_btn').click($.proxy(this.videoInsert, this)); + html = this.paragraphize.getSafes(html); + html = this.paragraphize.getSafesComments(html); + html = this.paragraphize.replaceBreaksToNewLines(html); + html = this.paragraphize.replaceBreaksToParagraphs(html); + html = this.paragraphize.clear(html); + html = this.paragraphize.restoreSafes(html); - setTimeout(function() + html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.paragraphize.blocks.join('|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>'); + + return $.trim(html); + }, + getSafes: function(html) { - $('#redactor_insert_video_area').focus(); + var $div = $('<div />').append(html); - }, 200); + // remove paragraphs in blockquotes + $div.find('blockquote p').replaceWith(function() + { + return $(this).append('<br />').contents(); + }); - }, this)); - }, - videoInsert: function () - { - var data = $('#redactor_insert_video_area').val(); - data = this.cleanStripTags(data); + html = $div.html(); - // parse if it is link on youtube & vimeo - var iframeStart = '<iframe width="500" height="281" src="', - iframeEnd = '" frameborder="0" allowfullscreen></iframe>'; + $div.find(this.paragraphize.blocks.join(', ')).each($.proxy(function(i,s) + { + this.paragraphize.z++; + this.paragraphize.safes[this.paragraphize.z] = s.outerHTML; + html = html.replace(s.outerHTML, '\n{replace' + this.paragraphize.z + '}'); - if (data.match(reUrlYoutube)) - { - data = data.replace(reUrlYoutube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd); - } - else if (data.match(reUrlVimeo)) - { - data = data.replace(reUrlVimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd); - } + }, this)); - this.selectionRestore(); + return html; + }, + getSafesComments: function(html) + { + var commentsMatches = html.match(/<!--([\w\W]*?)-->/gi); - var current = this.getBlock() || this.getCurrent(); + if (!commentsMatches) return html; - if (current) $(current).after(data) - else this.insertHtmlAdvanced(data, false); + $.each(commentsMatches, $.proxy(function(i,s) + { + this.paragraphize.z++; + this.paragraphize.safes[this.paragraphize.z] = s; + html = html.replace(s, '\n{replace' + this.paragraphize.z + '}'); + }, this)); - this.sync(); - this.modalClose(); - }, + return html; + }, + restoreSafes: function(html) + { + $.each(this.paragraphize.safes, function(i,s) + { + html = html.replace('{replace' + i + '}', s); + }); - - // LINK - linkShow: function() - { - this.selectionSave(); - - var callback = $.proxy(function() - { - // Predefined links - if (this.opts.predefinedLinks !== false) + return html; + }, + replaceBreaksToParagraphs: function(html) { - this.predefinedLinksStorage = {}; - var that = this; - $.getJSON(this.opts.predefinedLinks, function(data) + var htmls = html.split(new RegExp('\n', 'g'), -1); + + html = ''; + if (htmls) { - var $select = $('#redactor-predefined-links'); - $select .html(''); - $.each(data, function(key, val) + var len = htmls.length; + for (var i = 0; i < len; i++) { - that.predefinedLinksStorage[key] = val; - $select.append($('<option>').val(key).html(val.name)); - }); + if (!htmls.hasOwnProperty(i)) return; - $select.on('change', function() - { - var key = $(this).val(); - var name = '', url = ''; - if (key != 0) + if (htmls[i].search('{replace') == -1) { - name = that.predefinedLinksStorage[key].name; - url = that.predefinedLinksStorage[key].url; + htmls[i] = htmls[i].replace(/<p>\n\t?<\/p>/gi, ''); + htmls[i] = htmls[i].replace(/<p><\/p>/gi, ''); + + if (htmls[i] !== '') + { + html += '<p>' + htmls[i].replace(/^\n+|\n+$/g, "") + "</p>"; + } } + else html += htmls[i]; + } + } - $('#redactor_link_url').val(url); - $('#redactor_link_url_text').val(name); + return html; + }, + replaceBreaksToNewLines: function(html) + { + html = html.replace(/<br \/>\s*<br \/>/gi, "\n\n"); + html = html.replace(/<br\s?\/?>\n?<br\s?\/?>/gi, "\n<br /><br />"); - }); + html = html.replace(new RegExp("\r\n", 'g'), "\n"); + html = html.replace(new RegExp("\r", 'g'), "\n"); + html = html.replace(new RegExp("/\n\n+/"), 'g', "\n\n"); - $select.show(); - }); - } + return html; + }, + clear: function(html) + { + html = html.replace(new RegExp('</blockquote></p>', 'gi'), '</blockquote>'); + html = html.replace(new RegExp('<p></blockquote>', 'gi'), '</blockquote>'); + html = html.replace(new RegExp('<p><blockquote>', 'gi'), '<blockquote>'); + html = html.replace(new RegExp('<blockquote></p>', 'gi'), '<blockquote>'); - this.insert_link_node = false; + html = html.replace(new RegExp('<p><p ', 'gi'), '<p '); + html = html.replace(new RegExp('<p><p>', 'gi'), '<p>'); + html = html.replace(new RegExp('</p></p>', 'gi'), '</p>'); + html = html.replace(new RegExp('<p>\\s?</p>', 'gi'), ''); + html = html.replace(new RegExp("\n</p>", 'gi'), '</p>'); + html = html.replace(new RegExp('<p>\t?\t?\n?<p>', 'gi'), '<p>'); + html = html.replace(new RegExp('<p>\t*</p>', 'gi'), ''); - var sel = this.getSelection(); - var url = '', text = '', target = ''; - - var elem = this.getParent(); - var par = $(elem).parent().get(0); - if (par && par.tagName === 'A') - { - elem = par; + return html; } - - if (elem && elem.tagName === 'A') + }; + }, + paste: function() + { + return { + init: function(e) { - url = elem.href; - text = $(elem).text(); - target = elem.target; + if (!this.opts.cleanOnPaste) return; - this.insert_link_node = elem; - } - else text = sel.toString(); + this.rtePaste = true; - $('#redactor_link_url_text').val(text); + this.buffer.set(); + this.selection.save(); + this.utils.saveScroll(); - var thref = self.location.href.replace(/\/$/i, ''); - url = url.replace(thref, ''); - url = url.replace(/^\/#/, '#'); - url = url.replace('mailto:', ''); + this.paste.createPasteBox(); - // remove host from href - if (this.opts.linkProtocol === false) - { - var re = new RegExp('^(http|ftp|https)://' + self.location.host, 'i'); - url = url.replace(re, ''); - } + $(window).on('scroll.redactor-freeze', $.proxy(function() + { + $(window).scrollTop(this.saveBodyScroll); - // set url - $('#redactor_link_url').val(url); + }, this)); - if (target === '_blank') - { - $('#redactor_link_blank').prop('checked', true); - } + setTimeout($.proxy(function() + { + var html = this.$pasteBox.html(); - this.linkInsertPressed = false; - $('#redactor_insert_link_btn').on('click', $.proxy(this.linkProcess, this)); + this.$pasteBox.remove(); + this.selection.restore(); + this.utils.restoreScroll(); - setTimeout(function() - { - $('#redactor_link_url').focus(); + this.paste.insert(html); - }, 200); + $(window).off('scroll.redactor-freeze'); - }, this); + }, this), 1); - this.modalInit(this.opts.curLang.link, this.opts.modal_link, 460, callback); + }, + createPasteBox: function() + { + this.$pasteBox = $('<div>').html('').attr('contenteditable', 'true').css({ position: 'fixed', width: 0, top: 0, left: '-9999px' }); - }, - linkProcess: function() - { - if (this.linkInsertPressed) - { - return; - } + this.$box.parent().append(this.$pasteBox); + this.$pasteBox.focus(); + }, + insert: function(html) + { + html = this.core.setCallback('pasteBefore', html); - this.linkInsertPressed = true; - var target = '', targetBlank = ''; + // clean + html = (this.utils.isSelectAll()) ? this.clean.onPaste(html, false) : this.clean.onPaste(html); - var link = $('#redactor_link_url').val(); - var text = $('#redactor_link_url_text').val(); + html = this.core.setCallback('paste', html); - // mailto - if (link.search('@') != -1 && /(http|ftp|https):\/\//i.test(link) === false) - { - link = 'mailto:' + link; - } - // url, not anchor - else if (link.search('#') != 0) - { - if ($('#redactor_link_blank').prop('checked')) - { - target = ' target="_blank"'; - targetBlank = '_blank'; - } + if (this.utils.isSelectAll()) + { + this.insert.set(html, false); + } + else + { + this.insert.html(html, false); + } - // 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'); + this.utils.disableSelectAll(); + this.rtePaste = false; - if (link.search(re) == -1 && link.search(re2) == 0 && this.opts.linkProtocol) - { - link = this.opts.linkProtocol + link; - } - } + setTimeout($.proxy(this.clean.clearUnverified, this), 10); - text = text.replace(/<|>/g, ''); - var extra = '&nbsp;'; - if (this.browser('mozilla')) - { - extra = '&nbsp;'; - } + // clean empty spans + setTimeout($.proxy(function() + { + var spans = this.$editor.find('span'); + $.each(spans, function(i,s) + { + var html = s.innerHTML.replace(/[\u200B-\u200D\uFEFF]/, ''); + if (html === '' && s.attributes.length === 0) $(s).remove(); - this.linkInsert('<a href="' + link + '"' + target + '>' + text + '</a>' + extra, $.trim(text), link, targetBlank); + }); + }, this), 10); + } + }; }, - linkInsert: function (a, text, link, target) + placeholder: function() { - this.selectionRestore(); - - if (text !== '') - { - if (this.insert_link_node) + return { + enable: function() { - this.bufferSet(); + if (!this.placeholder.is()) return; - $(this.insert_link_node).text(text).attr('href', link); + this.$editor.attr('placeholder', this.$element.attr('placeholder')); - if (target !== '') + this.placeholder.toggle(); + this.$editor.on('keyup.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'); + }, + remove: function() + { + this.$editor.removeClass('redactor-placeholder'); + }, + is: function() + { + if (this.opts.placeholder) { - $(this.insert_link_node).attr('target', target); + return this.$element.attr('placeholder', this.opts.placeholder); } else { - $(this.insert_link_node).removeAttr('target'); + return !(typeof this.$element.attr('placeholder') == 'undefined' || this.$element.attr('placeholder') === ''); } } - else + }; + }, + progress: function() + { + return { + show: function() { - var $a = $(a).addClass('redactor-added-link'); - this.exec('inserthtml', this.outerHtml($a), false); - - var link = this.$editor.find('a.redactor-added-link'); - - link.removeAttr('style').removeClass('redactor-added-link').each(function() + $(document.body).append($('<div id="redactor-progress"><span></span></div>')); + $('#redactor-progress').fadeIn(); + }, + hide: function() + { + $('#redactor-progress').fadeOut(1500, function() { - if (this.className == '') $(this).removeAttr('class'); + $(this).remove(); }); - } - this.sync(); - } - - // link tooltip - setTimeout($.proxy(function() - { - if (this.opts.observeLinks) this.observeLinks(); - - }, this), 5); - - this.modalClose(); + }; }, - - // FILE - fileShow: function () + selection: function() { + return { + get: function() + { + this.sel = document.getSelection(); - this.selectionSave(); + if (document.getSelection && this.sel.getRangeAt && this.sel.rangeCount) + { + this.range = this.sel.getRangeAt(0); + } + else + { + this.range = document.createRange(); + } + }, + addRange: function() + { + try { + this.sel.removeAllRanges(); + } catch (e) {} - var callback = $.proxy(function() - { - var sel = this.getSelection(); + this.sel.addRange(this.range); + }, + getCurrent: function() + { + var el = false; + this.selection.get(); - var text = ''; - if (this.oldIE()) text = sel.text; - else text = sel.toString(); + if (this.sel && this.sel.rangeCount > 0) + { + el = this.sel.getRangeAt(0).startContainer; + } - $('#redactor_filename').val(text); + return this.utils.isRedactorParent(el); + }, + getParent: function(elem) + { + elem = elem || this.selection.getCurrent(); + if (elem) + { + return this.utils.isRedactorParent($(elem).parent()[0]); + } - // dragupload - if (!this.isMobile() && !this.isIPad()) + return false; + }, + getBlock: function(node) { - this.draguploadInit('#redactor_file', { - url: this.opts.fileUpload, - uploadFields: this.opts.uploadFields, - success: $.proxy(this.fileCallback, this), - error: $.proxy( function(obj, json) - { - this.callback('fileUploadError', json); + node = node || this.selection.getCurrent(); - }, this), - uploadParam: this.opts.fileUploadParam - }); - } - - this.uploadInit('redactor_file', { - auto: true, - url: this.opts.fileUpload, - success: $.proxy(this.fileCallback, this), - error: $.proxy(function(obj, json) + while (node) { - this.callback('fileUploadError', json); + if (this.utils.isBlockTag(node.tagName)) + { + return ($(node).hasClass('redactor-editor')) ? false : node; + } - }, this) - }); + node = node.parentNode; + } - }, this); + return false; + }, + getInlines: function(nodes) + { + this.selection.get(); - this.modalInit(this.opts.curLang.file, this.opts.modal_file, 500, callback); - }, - fileCallback: function(json) - { + if (this.range && this.range.collapsed) + { + return false; + } - this.selectionRestore(); + var inlines = []; + nodes = (typeof nodes == 'undefined') ? this.selection.getNodes() : nodes; + var inlineTags = this.opts.inlineTags; + inlineTags.push('span'); + $.each(nodes, $.proxy(function(i,node) + { + if ($.inArray(node.tagName.toLowerCase(), inlineTags) != -1) + { + inlines.push(node); + } - if (json !== false) - { + }, this)); - var text = $('#redactor_filename').val(); - if (text === '') text = json.filename; - - var link = '<a href="' + json.filelink + '" id="filelink-marker">' + text + '</a>'; - - // chrome fix - if (this.browser('webkit') && !!this.window.chrome) + return (inlines.length === 0) ? false : inlines; + }, + getBlocks: function(nodes) { - link = link + '&nbsp;'; - } + this.selection.get(); - this.execCommand('inserthtml', link, false); + if (this.range && this.range.collapsed) + { + return [this.selection.getBlock()]; + } - var linkmarker = $(this.$editor.find('a#filelink-marker')); - if (linkmarker.size() != 0) linkmarker.removeAttr('id'); - else linkmarker = false; + var blocks = []; + nodes = (typeof nodes == 'undefined') ? this.selection.getNodes() : nodes; + $.each(nodes, $.proxy(function(i,node) + { + if (this.utils.isBlock(node)) + { + this.selection.lastBlock = node; + blocks.push(node); + } - this.sync(); + }, this)); - // file upload callback - this.callback('fileUpload', linkmarker, json); - } + return (blocks.length === 0) ? [this.selection.getBlock()] : blocks; + }, + getLastBlock: function() + { + return this.selection.lastBlock; + }, + getNodes: function() + { + this.selection.get(); - this.modalClose(); - }, + var startNode = this.selection.getNodesMarker(1); + var endNode = this.selection.getNodesMarker(2); - // IMAGE - imageShow: function() - { + this.selection.setNodesMarker(this.range, startNode, true); - this.selectionSave(); + if (this.range.collapsed === false) + { + this.selection.setNodesMarker(this.range, endNode, false); + } + else + { + endNode = startNode; + } - var callback = $.proxy(function() - { - // json - if (this.opts.imageGetJson) - { + var nodes = []; + var counter = 0; - $.getJSON(this.opts.imageGetJson, $.proxy(function(data) + var self = this; + this.$editor.find('*').each(function() { - var folders = {}, count = 0; - - // folders - $.each(data, $.proxy(function(key, val) + if (this == startNode) { - if (typeof val.folder !== 'undefined') + var parent = $(this).parent(); + if (parent.length !== 0 && parent[0].tagName != 'BODY' && self.utils.isRedactorParent(parent[0])) { - count++; - folders[val.folder] = count; + nodes.push(parent[0]); } - }, this)); - - var folderclass = false; - $.each(data, $.proxy(function(key, val) + nodes.push(this); + counter = 1; + } + else { - // title - var thumbtitle = ''; - if (typeof val.title !== 'undefined') thumbtitle = val.title; - - var folderkey = 0; - if (!$.isEmptyObject(folders) && typeof val.folder !== 'undefined') + if (counter > 0) { - folderkey = folders[val.folder]; - if (folderclass === false) folderclass = '.redactorfolder' + folderkey; + nodes.push(this); + counter = counter + 1; } + } - var img = $('<img src="' + val.thumb + '" class="redactorfolder redactorfolder' + folderkey + '" rel="' + val.image + '" title="' + thumbtitle + '" />'); - $('#redactor_image_box').append(img); - $(img).click($.proxy(this.imageThumbClick, this)); - - }, this)); - - // folders - if (!$.isEmptyObject(folders)) + if (this == endNode) { - $('.redactorfolder').hide(); - $(folderclass).show(); - - var onchangeFunc = function(e) - { - $('.redactorfolder').hide(); - $('.redactorfolder' + $(e.target).val()).show(); - }; - - var select = $('<select id="redactor_image_box_select">'); - $.each( folders, function(k, v) - { - select.append( $('<option value="' + v + '">' + k + '</option>')); - }); - - $('#redactor_image_box').before(select); - select.change(onchangeFunc); + return false; } - }, this)); - } - else - { - $('#redactor-tab-control-2').remove(); - } + }); - if (this.opts.imageUpload || this.opts.s3) - { - // dragupload - if (!this.isMobile() && !this.isIPad() && this.opts.s3 === false) + var finalNodes = []; + var len = nodes.length; + for (var i = 0; i < len; i++) { - if ($('#redactor_file' ).length) + if (nodes[i].id != 'nodes-marker-1' && nodes[i].id != 'nodes-marker-2') { - this.draguploadInit('#redactor_file', { - url: this.opts.imageUpload, - uploadFields: this.opts.uploadFields, - success: $.proxy(this.imageCallback, this), - error: $.proxy(function(obj, json) - { - this.callback('imageUploadError', json); - - }, this), - uploadParam: this.opts.imageUploadParam - }); + finalNodes.push(nodes[i]); } } - if (this.opts.s3 === false) - { - // ajax upload - this.uploadInit('redactor_file', { - auto: true, - url: this.opts.imageUpload, - success: $.proxy(this.imageCallback, this), - error: $.proxy(function(obj, json) - { - this.callback('imageUploadError', json); + this.selection.removeNodesMarkers(); - }, this) - }); - } - // s3 upload - else - { - $('#redactor_file').on('change.redactor', $.proxy(this.s3handleFileSelect, this)); - } + return finalNodes; - } - else + }, + getNodesMarker: function(num) { - $('.redactor_tab').hide(); - if (!this.opts.imageGetJson) - { - $('#redactor_tabs').remove(); - $('#redactor_tab3').show(); - } - else - { - $('#redactor-tab-control-1').remove(); - $('#redactor-tab-control-2').addClass('redactor_tabs_act'); - $('#redactor_tab2').show(); - } - } + 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(); - if (!this.opts.imageTabLink && (this.opts.imageUpload || this.opts.imageGetJson)) + try { + range.collapse(type); + range.insertNode(node); + } + catch (e) {} + }, + removeNodesMarkers: function() { - $('#redactor-tab-control-3').hide(); - } + $(document).find('span.redactor-nodes-marker').remove(); + this.$editor.find('span.redactor-nodes-marker').remove(); + }, + fromPoint: function(start, end) + { + this.caret.setOffset(start, end); + }, + wrap: function(tag) + { + this.selection.get(); - $('#redactor_upload_btn').click($.proxy(this.imageCallbackLink, this)); + if (this.range.collapsed) return false; - if (!this.opts.imageUpload && !this.opts.imageGetJson) + var wrapper = document.createElement(tag); + wrapper.appendChild(this.range.extractContents()); + this.range.insertNode(wrapper); + + return wrapper; + }, + selectElement: function(node) { - setTimeout(function() - { - $('#redactor_file_link').focus(); + this.caret.set(node, 0, node, 1); + }, + selectAll: function() + { + this.selection.get(); + this.range.selectNodeContents(this.$editor[0]); + this.selection.addRange(); + }, + remove: function() + { + this.selection.get(); + this.sel.removeAllRanges(); + }, + save: function() + { + this.selection.createMarkers(); + }, + createMarkers: function() + { + this.selection.get(); - }, 200); - } + var node1 = this.selection.getMarker(1); - }, this); + this.selection.setMarker(this.range, node1, true); - this.modalInit(this.opts.curLang.image, this.opts.modal_image, 610, callback); + if (this.range.collapsed === false) + { + var node2 = this.selection.getMarker(2); + this.selection.setMarker(this.range, node2, false); + } - }, - imageEdit: function(image) - { - var $el = image; - var parent = $el.parent().parent(); + this.savedSel = this.$editor.html(); + }, + getMarker: function(num) + { + if (typeof num == 'undefined') num = 1; - var callback = $.proxy(function() - { - $('#redactor_file_alt').val($el.attr('alt')); - $('#redactor_image_edit_src').attr('href', $el.attr('src')); - - if ($el.css('display') == 'block' && $el.css('float') == 'none') + return $('<span id="selection-marker-' + num + '" class="redactor-selection-marker" data-verified="redactor">' + this.opts.invisibleSpace + '</span>')[0]; + }, + getMarkerAsHtml: function(num) { - $('#redactor_form_image_align').val('center'); - } - else + return this.utils.getOuterHtml(this.selection.getMarker(num)); + }, + setMarker: function(range, node, type) { - $('#redactor_form_image_align').val($el.css('float')); - } + range = range.cloneRange(); - if ($(parent).get(0).tagName === 'A') + try { + range.collapse(type); + range.insertNode(node); + } + catch (e) + { + this.focus.setStart(); + } + }, + restore: function() { - $('#redactor_file_link').val($(parent).attr('href')); + var node1 = this.$editor.find('span#selection-marker-1'); + var node2 = this.$editor.find('span#selection-marker-2'); - if ($(parent).attr('target') == '_blank') + if (node1.length !== 0 && node2.length !== 0) { - $('#redactor_link_blank').prop('checked', true); + this.caret.set(node1, 0, node2, 0); } - } + else if (node1.length !== 0) + { + this.caret.set(node1, 0, node1, 0); + } + else + { + this.$editor.focus(); + } - $('#redactor_image_delete_btn').click($.proxy(function() + this.selection.removeMarkers(); + this.savedSel = false; + + }, + removeMarkers: function() { - this.imageRemove($el); + this.$editor.find('span.redactor-selection-marker').remove(); + }, + getText: function() + { + this.selection.get(); - }, this)); - - $('#redactorSaveBtn').click($.proxy(function() + return this.sel.toString(); + }, + getHtml: function() { - this.imageSave($el); + var html = ''; - }, this)); + this.selection.get(); + if (this.sel.rangeCount) + { + var container = document.createElement('div'); + var len = this.sel.rangeCount; + for (var i = 0; i < len; ++i) + { + container.appendChild(this.sel.getRangeAt(i).cloneContents()); + } - }, this); + html = container.innerHTML; + } - this.modalInit(this.opts.curLang.edit, this.opts.modal_image_edit, 380, callback); - + return this.clean.onSync(html); + } + }; }, - imageRemove: function(el) + shortcuts: function() { - var parentLink = $(el).parent().parent(); - var parent = $(el).parent(); - var parentEl = false; + return { + init: function(e, key) + { + // disable browser's hot keys for bold and italic + if (!this.opts.shortcuts) + { + if ((e.ctrlKey || e.metaKey) && (key === 66 || key === 73)) e.preventDefault(); + return false; + } - if (parentLink.length && parentLink[0].tagName === 'A') - { - parentEl = true; - $(parentLink).remove(); - } - else if (parent.length && parent[0].tagName === 'A') - { - parentEl = true; - $(parent).remove(); - } - else - { - $(el).remove(); - } + $.each(this.opts.shortcuts, $.proxy(function(str, command) + { + var keys = str.split(','); + var len = keys.length; + for (var i = 0; i < len; i++) + { + if (typeof keys[i] === 'string') + { + this.shortcuts.handler(e, $.trim(keys[i]), $.proxy(function() + { + var func; + if (command.func.search(/\./) != '-1') + { + func = command.func.split('.'); + if (typeof this[func[0]] != 'undefined') + { + this[func[0]][func[1]].apply(this, command.params); + } + } + else + { + this[command.func].apply(this, command.params); + } - if (parent.length && parent[0].tagName === 'P') - { - this.focusWithSaveScroll(); + }, this)); + } - if (parentEl === false) this.selectionStart(parent); - } + } - // delete callback - this.callback('imageDelete', el); + }, this)); + }, + handler: function(e, keys, origHandler) + { + // based on https://github.com/jeresig/jquery.hotkeys + var hotkeysSpecialKeys = + { + 8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 59: ";", 61: "=", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 173: "-", 186: ";", 187: "=", + 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'" + }; - this.modalClose(); - this.sync(); - }, - imageSave: function(el) - { - this.imageResizeHide(false); - var $el = $(el); - var parent = $el.parent(); + var hotkeysShiftNums = + { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + }; - $el.attr('alt', $('#redactor_file_alt').val()); + keys = keys.toLowerCase().split(" "); + var special = hotkeysSpecialKeys[e.keyCode], + character = String.fromCharCode( e.which ).toLowerCase(), + modif = "", possible = {}; - var floating = $('#redactor_form_image_align').val(); - var margin = ''; + $.each([ "alt", "ctrl", "meta", "shift"], function(index, specialKey) + { + if (e[specialKey + 'Key'] && special !== specialKey) + { + modif += specialKey + '+'; + } + }); - if (floating === 'left') - { - margin = '0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ' 0'; - $el.css({ 'float': 'left', 'margin': margin }); - } - else if (floating === 'right') - { - margin = '0 0 ' + this.opts.imageFloatMargin + ' ' + this.opts.imageFloatMargin + ''; - $el.css({ 'float': 'right', 'margin': margin }); - } - else if (floating === 'center') - { - $el.css({ 'float': '', 'display': 'block', 'margin': 'auto' }); - } - else - { - $el.css({ 'float': '', 'display': '', 'margin': '' }); - } - // as link - var link = $.trim($('#redactor_file_link').val()); - if (link !== '') - { - var target = false; - if ($('#redactor_link_blank').prop('checked')) - { - target = true; - } - - if (parent.get(0).tagName !== 'A') - { - var a = $('<a href="' + link + '">' + this.outerHtml(el) + '</a>'); - - if (target) + if (special) possible[modif + special] = true; + if (character) { - a.attr('target', '_blank'); + possible[modif + character] = true; + possible[modif + hotkeysShiftNums[character]] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if (modif === "shift+") + { + possible[hotkeysShiftNums[character]] = true; + } } - $el.replaceWith(a); - } - else - { - parent.attr('href', link); - if (target) + for (var i = 0, len = keys.length; i < len; i++) { - parent.attr('target', '_blank'); + if (possible[keys[i]]) + { + e.preventDefault(); + return origHandler.apply(this, arguments); + } } - else - { - parent.removeAttr('target'); - } } - } - else - { - if (parent.get(0).tagName === 'A') - { - parent.replaceWith(this.outerHtml(el)); - } - } - - this.modalClose(); - this.observeImages(); - this.sync(); - + }; }, - imageResizeHide: function(e) + tabifier: function() { - if (e !== false && $(e.target).parent().size() != 0 && $(e.target).parent()[0].id === 'redactor-image-box') - { - return false; - } + return { + get: function(code) + { + if (!this.opts.tabifier) return code; - var imageBox = this.$editor.find('#redactor-image-box'); - if (imageBox.size() == 0) - { - return false; - } + // clean setup + var ownLine = ['area', 'body', 'head', 'hr', 'i?frame', 'link', 'meta', 'noscript', 'style', 'script', 'table', 'tbody', 'thead', 'tfoot']; + var contOwnLine = ['li', 'dt', 'dt', 'h[1-6]', 'option', 'script']; + var newLevel = ['blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'p', 'pre', 'select', 'td', 'th', 'tr', 'ul']; - this.$editor.find('#redactor-image-editter, #redactor-image-resizer').remove(); + 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('|' ) + ')[ >]'); - imageBox.find('img').css({ - marginTop: imageBox[0].style.marginTop, - marginBottom: imageBox[0].style.marginBottom, - marginLeft: imageBox[0].style.marginLeft, - marginRight: imageBox[0].style.marginRight - }); + var i = 0, + codeLength = code.length, + point = 0, + start = null, + end = null, + tag = '', + out = '', + cont = ''; - imageBox.css('margin', ''); + this.tabifier.cleanlevel = 0; - - imageBox.find('img').css('opacity', ''); - imageBox.replaceWith(function() - { - return $(this).contents(); - }); - - $(document).off('click.redactor-image-resize-hide'); - this.$editor.off('click.redactor-image-resize-hide'); - this.$editor.off('keydown.redactor-image-delete'); - - this.sync() - - }, - imageResize: function(image) - { - var $image = $(image); - - $image.on('mousedown', $.proxy(function() - { - this.imageResizeHide(false); - }, this)); - - $image.on('dragstart', $.proxy(function() - { - this.$editor.on('drop.redactor-image-inside-drop', $.proxy(function() - { - setTimeout($.proxy(function() + for (; i < codeLength; i++) { - this.observeImages(); - this.$editor.off('drop.redactor-image-inside-drop'); - this.sync(); + point = i; - }, this), 1); + // if no more tags, copy and exit + if (-1 == code.substr(i).indexOf( '<' )) + { + out += code.substr(i); - },this)); - }, this)); + return this.tabifier.finish(out); + } - $image.on('click', $.proxy(function(e) - { - if (this.$editor.find('#redactor-image-box').size() != 0) - { - return false; - } + // copy verbatim until a tag + while (point < codeLength && code.charAt(point) != '<') + { + point++; + } - var clicked = false, - start_x, - start_y, - ratio = $image.width() / $image.height(), - min_w = 20, - min_h = 10; + if (i != point) + { + cont = code.substr(i, point - i); + if (!cont.match(/^\s{2,}$/g)) + { + if ('\n' == out.charAt(out.length - 1)) out += this.tabifier.getTabs(); + else if ('\n' == cont.charAt(0)) + { + out += '\n' + this.tabifier.getTabs(); + cont = cont.replace(/^\s+/, ''); + } - var imageResizer = this.imageResizeControls($image); + out += cont; + } - // resize - var isResizing = false; - if (imageResizer !== false) - { - imageResizer.on('mousedown', function(e) - { - isResizing = true; - e.preventDefault(); + if (cont.match(/\n/)) out += '\n' + this.tabifier.getTabs(); + } - ratio = $image.width() / $image.height(); + start = point; - start_x = Math.round(e.pageX - $image.eq(0).offset().left); - start_y = Math.round(e.pageY - $image.eq(0).offset().top); - - }); - - $(this.document.body).on('mousemove', $.proxy(function(e) - { - if (isResizing) + // find the end of the tag + while (point < codeLength && '>' != code.charAt(point)) { - var mouse_x = Math.round(e.pageX - $image.eq(0).offset().left) - start_x; - var mouse_y = Math.round(e.pageY - $image.eq(0).offset().top) - start_y; + point++; + } - var div_h = $image.height(); + tag = code.substr(start, point - start); + i = point; - var new_h = parseInt(div_h, 10) + mouse_y; - var new_w = Math.round(new_h * ratio); + var t; - if (new_w > min_w) + if ('!--' == tag.substr(1, 3)) + { + if (!tag.match(/--$/)) { - $image.width(new_w); - - if (new_w < 100) + while ('-->' != code.substr(point, 3)) { - this.imageEditter.css({ - marginTop: '-7px', - marginLeft: '-13px', - fontSize: '9px', - padding: '3px 5px' - }); + point++; } - else - { - this.imageEditter.css({ - marginTop: '-11px', - marginLeft: '-18px', - fontSize: '11px', - padding: '7px 10px' - }); - } + point += 2; + tag = code.substr(start, point - start); + i = point; } - start_x = Math.round(e.pageX - $image.eq(0).offset().left); - start_y = Math.round(e.pageY - $image.eq(0).offset().top); + if ('\n' != out.charAt(out.length - 1)) out += '\n'; - this.sync() + out += this.tabifier.getTabs(); + out += tag + '>\n'; } - }, this)).on('mouseup', function() - { - isResizing = false; - }); - } + else if ('!' == tag[1]) + { + out = this.tabifier.placeTag(tag + '>', out); + } + else if ('?' == tag[1]) + { + out += tag + '>\n'; + } + else if (t = tag.match(/^<(script|style|pre)/i)) + { + t[1] = t[1].toLowerCase(); + tag = this.tabifier.cleanTag(tag); + out = this.tabifier.placeTag(tag, out); + end = String(code.substr(i + 1)).toLowerCase().indexOf('</' + t[1]); + if (end) + { + cont = code.substr(i + 1, end); + i += end; + out += cont; + } + } + else + { + tag = this.tabifier.cleanTag(tag); + out = this.tabifier.placeTag(tag, out); + } + } - this.$editor.on('keydown.redactor-image-delete', $.proxy(function(e) + return this.tabifier.finish(out); + }, + getTabs: function() { - var key = e.which; - - if (this.keyCode.BACKSPACE == key || this.keyCode.DELETE == key) + var s = ''; + for ( var j = 0; j < this.tabifier.cleanlevel; j++ ) { - this.bufferSet(false); - this.imageResizeHide(false); - this.imageRemove($image); + s += '\t'; } - }, this)); + return s; + }, + finish: function(code) + { + code = code.replace(/\n\s*\n/g, '\n'); + code = code.replace(/^[\s\n]*/, ''); + code = code.replace(/[\s\n]*$/, ''); + code = code.replace(/<script(.*?)>\n<\/script>/gi, '<script$1></script>'); - $(document).on('click.redactor-image-resize-hide', $.proxy(this.imageResizeHide, this)); - this.$editor.on('click.redactor-image-resize-hide', $.proxy(this.imageResizeHide, this)); + this.tabifier.cleanlevel = 0; + return code; + }, + cleanTag: function (tag) + { + var tagout = ''; + tag = tag.replace(/\n/g, ' '); + tag = tag.replace(/\s{2,}/g, ' '); + tag = tag.replace(/^\s+|\s+$/g, ' '); - }, this)); - }, - imageResizeControls: function($image) - { - var imageBox = $('<span id="redactor-image-box" data-redactor="verified">'); - imageBox.css({ - position: 'relative', - display: 'inline-block', - lineHeight: 0, - outline: '1px dashed rgba(0, 0, 0, .6)', - 'float': $image.css('float') - }); - imageBox.attr('contenteditable', false); + var suffix = ''; + if (tag.match(/\/$/)) + { + suffix = '/'; + tag = tag.replace(/\/+$/, ''); + } - if ($image[0].style.margin != 'auto') - { - imageBox.css({ - marginTop: $image[0].style.marginTop, - marginBottom: $image[0].style.marginBottom, - marginLeft: $image[0].style.marginLeft, - marginRight: $image[0].style.marginRight - }); + var m; + while (m = /\s*([^= ]+)(?:=((['"']).*?\3|[^ ]+))?/.exec(tag)) + { + if (m[2]) tagout += m[1].toLowerCase() + '=' + m[2]; + else if (m[1]) tagout += m[1].toLowerCase(); - $image.css('margin', ''); - } - else - { - imageBox.css({ 'display': 'block', 'margin': 'auto' }); - } + tagout += ' '; + tag = tag.substr(m[0].length); + } - $image.css('opacity', .5).after(imageBox); + 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'; + } - // editter - this.imageEditter = $('<span id="redactor-image-editter" data-redactor="verified">' + this.opts.curLang.edit + '</span>'); - this.imageEditter.css({ - position: 'absolute', - zIndex: 5, - top: '50%', - left: '50%', - marginTop: '-11px', - marginLeft: '-18px', - lineHeight: 1, - backgroundColor: '#000', - color: '#fff', - fontSize: '11px', - padding: '7px 10px', - cursor: 'pointer' - }); - this.imageEditter.attr('contenteditable', false); - this.imageEditter.on('click', $.proxy(function() - { - this.imageEdit($image); - }, this)); - imageBox.append(this.imageEditter); + if (nl && '/' == tag.charAt(1)) this.tabifier.cleanlevel--; + if ('\n' == out.charAt(out.length - 1)) out += this.tabifier.getTabs(); + if (nl && '/' != tag.charAt(1)) this.tabifier.cleanlevel++; - // resizer - if (this.opts.imageResizable) - { - var imageResizer = $('<span id="redactor-image-resizer" data-redactor="verified"></span>'); - imageResizer.css({ - position: 'absolute', - zIndex: 2, - lineHeight: 1, - cursor: 'nw-resize', - bottom: '-4px', - right: '-5px', - border: '1px solid #fff', - backgroundColor: '#000', - width: '8px', - height: '8px' - }); - imageResizer.attr('contenteditable', false); - imageBox.append(imageResizer); + out += tag; - imageBox.append($image); + if (tag.match(this.tabifier.lineAfter) || tag.match(this.tabifier.newLevel)) + { + out = out.replace(/ *$/, ''); + out += '\n'; + } - return imageResizer; - } - else - { - imageBox.append($image); - - return false; - } + return out; + } + }; }, - imageThumbClick: function(e) + tidy: function() { - var img = '<img id="image-marker" src="' + $(e.target).attr('rel') + '" alt="' + $(e.target).attr('title') + '" />'; + return { + setupAllowed: function() + { + if (this.opts.allowedTags) this.opts.deniedTags = false; + if (this.opts.allowedAttr) this.opts.removeAttr = false; - var parent = this.getParent(); - if (this.opts.paragraphy && $(parent).closest('li').size() == 0) img = '<p>' + img + '</p>'; + if (this.opts.linebreaks) return; - this.imageInsert(img, true); - }, - imageCallbackLink: function() - { - var val = $('#redactor_file_link').val(); + var tags = ['p', 'section']; + if (this.opts.allowedTags) this.tidy.addToAllowed(tags); + if (this.opts.deniedTags) this.tidy.removeFromDenied(tags); - if (val !== '') - { - var data = '<img id="image-marker" src="' + val + '" />'; - if (this.opts.linebreaks === false) data = '<p>' + data + '</p>'; - - this.imageInsert(data, true); - - } - else this.modalClose(); - }, - imageCallback: function(data) - { - this.imageInsert(data); - }, - imageInsert: function(json, link) - { - this.selectionRestore(); - - if (json !== false) - { - var html = ''; - if (link !== true) + }, + addToAllowed: function(tags) { - html = '<img id="image-marker" src="' + json.filelink + '" />'; - - var parent = this.getParent(); - if (this.opts.paragraphy && $(parent).closest('li').size() == 0) + var len = tags.length; + for (var i = 0; i < len; i++) { - html = '<p>' + html + '</p>'; + if ($.inArray(tags[i], this.opts.allowedTags) == -1) + { + this.opts.allowedTags.push(tags[i]); + } } - } - else + }, + removeFromDenied: function(tags) { - html = json; - } + var len = tags.length; + for (var i = 0; i < len; i++) + { + var pos = $.inArray(tags[i], this.opts.deniedTags); + if (pos != -1) + { + this.opts.deniedTags.splice(pos, 1); + } + } + }, + load: function(html, options) + { + this.tidy.settings = { + deniedTags: this.opts.deniedTags, + allowedTags: this.opts.allowedTags, + removeComments: this.opts.removeComments, + replaceTags: this.opts.replaceTags, + replaceStyles: this.opts.replaceStyles, + removeDataAttr: this.opts.removeDataAttr, + removeAttr: this.opts.removeAttr, + allowedAttr: this.opts.allowedAttr, + removeWithoutAttr: this.opts.removeWithoutAttr, + removeEmpty: this.opts.removeEmpty + }; - this.execCommand('inserthtml', html, false); + $.extend(this.tidy.settings, options); - var image = $(this.$editor.find('img#image-marker')); + html = this.tidy.removeComments(html); - if (image.length) image.removeAttr('id'); - else image = false; + // create container + this.tidy.$div = $('<div />').append(html); - this.sync(); + // clean + this.tidy.replaceTags(); + this.tidy.replaceStyles(); + this.tidy.removeTags(); - // upload image callback - link !== true && this.callback('imageUpload', image, json); - } + this.tidy.removeAttr(); + this.tidy.removeEmpty(); + this.tidy.removeParagraphsInLists(); + this.tidy.removeDataAttr(); + this.tidy.removeWithoutAttr(); - this.modalClose(); - this.observeImages(); - }, + html = this.tidy.$div.html(); + this.tidy.$div.remove(); - // PROGRESS BAR - buildProgressBar: function() - { - if ($('#redactor-progress').size() != 0) return; + return html; + }, + removeComments: function(html) + { + if (!this.tidy.settings.removeComments) return html; - this.$progressBar = $('<div id="redactor-progress"><span></span></div>'); - $(document.body).append(this.$progressBar); - }, - showProgressBar: function() - { - this.buildProgressBar(); - $('#redactor-progress').fadeIn(); - }, - hideProgressBar: function() - { - $('#redactor-progress').fadeOut(1500); - }, + return html.replace(/<!--[\s\S]*?-->/gi, ''); + }, + replaceTags: function(html) + { + if (!this.tidy.settings.replaceTags) return html; - // MODAL - modalTemplatesInit: function() - { - $.extend( this.opts, - { - modal_file: String() - + '<section id="redactor-modal-file-insert">' - + '<form id="redactorUploadFileForm" method="post" action="" enctype="multipart/form-data">' - + '<label>' + this.opts.curLang.filename + '</label>' - + '<input type="text" id="redactor_filename" class="redactor_input" />' - + '<div style="margin-top: 7px;">' - + '<input type="file" id="redactor_file" name="' + this.opts.fileUploadParam + '" />' - + '</div>' - + '</form>' - + '</section>', + var len = this.tidy.settings.replaceTags.length; + var replacement = [], rTags = []; + for (var i = 0; i < len; i++) + { + rTags.push(this.tidy.settings.replaceTags[i][1]); + replacement.push(this.tidy.settings.replaceTags[i][0]); + } - modal_image_edit: String() - + '<section id="redactor-modal-image-edit">' - + '<label>' + this.opts.curLang.title + '</label>' - + '<input type="text" id="redactor_file_alt" class="redactor_input" />' - + '<label>' + this.opts.curLang.link + '</label>' - + '<input type="text" id="redactor_file_link" class="redactor_input" />' - + '<label><input type="checkbox" id="redactor_link_blank"> ' + this.opts.curLang.link_new_tab + '</label>' - + '<label>' + this.opts.curLang.image_position + '</label>' - + '<select id="redactor_form_image_align">' - + '<option value="none">' + this.opts.curLang.none + '</option>' - + '<option value="left">' + this.opts.curLang.left + '</option>' - + '<option value="center">' + this.opts.curLang.center + '</option>' - + '<option value="right">' + this.opts.curLang.right + '</option>' - + '</select>' - + '</section>' - + '<footer>' - + '<button id="redactor_image_delete_btn" class="redactor_modal_btn redactor_modal_delete_btn">' + this.opts.curLang._delete + '</button>' - + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>' - + '<button id="redactorSaveBtn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.save + '</button>' - + '</footer>', + this.tidy.$div.find(replacement.join(',')).each($.proxy(function(n,s) + { + var tag = rTags[n]; + $(s).replaceWith(function() + { + var replaced = $('<' + tag + ' />').append($(this).contents()); - modal_image: String() - + '<section id="redactor-modal-image-insert">' - + '<div id="redactor_tabs">' - + '<a href="#" id="redactor-tab-control-1" class="redactor_tabs_act">' + this.opts.curLang.upload + '</a>' - + '<a href="#" id="redactor-tab-control-2">' + this.opts.curLang.choose + '</a>' - + '<a href="#" id="redactor-tab-control-3">' + this.opts.curLang.link + '</a>' - + '</div>' - + '<form id="redactorInsertImageForm" method="post" action="" enctype="multipart/form-data">' - + '<div id="redactor_tab1" class="redactor_tab">' - + '<input type="file" id="redactor_file" name="' + this.opts.imageUploadParam + '" />' - + '</div>' - + '<div id="redactor_tab2" class="redactor_tab" style="display: none;">' - + '<div id="redactor_image_box"></div>' - + '</div>' - + '</form>' - + '<div id="redactor_tab3" class="redactor_tab" style="display: none;">' - + '<label>' + this.opts.curLang.image_web_link + '</label>' - + '<input type="text" name="redactor_file_link" id="redactor_file_link" class="redactor_input" /><br><br>' - + '</div>' - + '</section>' - + '<footer>' - + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>' - + '<button class="redactor_modal_btn redactor_modal_action_btn" id="redactor_upload_btn">' + this.opts.curLang.insert + '</button>' - + '</footer>', + for (var i = 0; i < this.attributes.length; i++) + { + replaced.attr(this.attributes[i].name, this.attributes[i].value); + } - modal_link: String() - + '<section id="redactor-modal-link-insert">' - + '<select id="redactor-predefined-links" style="width: 99.5%; display: none;"></select>' - + '<label>URL</label>' - + '<input type="text" class="redactor_input" id="redactor_link_url" />' - + '<label>' + this.opts.curLang.text + '</label>' - + '<input type="text" class="redactor_input" id="redactor_link_url_text" />' - + '<label><input type="checkbox" id="redactor_link_blank"> ' + this.opts.curLang.link_new_tab + '</label>' - + '</section>' - + '<footer>' - + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>' - + '<button id="redactor_insert_link_btn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.insert + '</button>' - + '</footer>', + return replaced; + }); - modal_table: String() - + '<section id="redactor-modal-table-insert">' - + '<label>' + this.opts.curLang.rows + '</label>' - + '<input type="text" size="5" value="2" id="redactor_table_rows" />' - + '<label>' + this.opts.curLang.columns + '</label>' - + '<input type="text" size="5" value="3" id="redactor_table_columns" />' - + '</section>' - + '<footer>' - + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>' - + '<button id="redactor_insert_table_btn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.insert + '</button>' - + '</footer>', + }, this)); - modal_video: String() - + '<section id="redactor-modal-video-insert">' - + '<form id="redactorInsertVideoForm">' - + '<label>' + this.opts.curLang.video_html_code + '</label>' - + '<textarea id="redactor_insert_video_area" style="width: 99%; height: 160px;"></textarea>' - + '</form>' - + '</section>' - + '<footer>' - + '<button class="redactor_modal_btn redactor_btn_modal_close">' + this.opts.curLang.cancel + '</button>' - + '<button id="redactor_insert_video_btn" class="redactor_modal_btn redactor_modal_action_btn">' + this.opts.curLang.insert + '</button>' - + '</footer>' + return html; + }, + replaceStyles: function() + { + if (!this.tidy.settings.replaceStyles) return; - }); - }, - modalInit: function(title, content, width, callback) - { - this.modalSetOverlay(); + var len = this.tidy.settings.replaceStyles.length; + this.tidy.$div.find('span').each($.proxy(function(n,s) + { + var $el = $(s); + var style = $el.attr('style'); + for (var i = 0; i < len; i++) + { + if (style && style.match(new RegExp('^' + this.tidy.settings.replaceStyles[i][0], 'i'))) + { + var tagName = this.tidy.settings.replaceStyles[i][1]; + $el.replaceWith(function() + { + var tag = document.createElement(tagName); + return $(tag).append($(this).contents()); + }); + } + } - this.$redactorModalWidth = width; - this.$redactorModal = $('#redactor_modal'); + }, this)); - if (!this.$redactorModal.length) - { - this.$redactorModal = $('<div id="redactor_modal" style="display: none;" />'); - this.$redactorModal.append($('<div id="redactor_modal_close">&times;</div>')); - this.$redactorModal.append($('<header id="redactor_modal_header" />')); - this.$redactorModal.append($('<div id="redactor_modal_inner" />')); - this.$redactorModal.appendTo(document.body); - } + }, + removeTags: function() + { + if (!this.tidy.settings.deniedTags && this.tidy.settings.allowedTags) + { + this.tidy.$div.find('*').not(this.tidy.settings.allowedTags.join(',')).each(function(i, s) + { + if (s.innerHTML === '') $(s).remove(); + else $(s).contents().unwrap(); + }); + } - $('#redactor_modal_close').on('click', $.proxy(this.modalClose, this)); - $(document).on('keyup', $.proxy(this.modalCloseHandler, this)); - this.$editor.on('keyup', $.proxy(this.modalCloseHandler, this)); + if (this.tidy.settings.deniedTags) + { + this.tidy.$div.find(this.tidy.settings.deniedTags.join(',')).each(function(i, s) + { + if (s.innerHTML === '') $(s).remove(); + else $(s).contents().unwrap(); + }); + } + }, + removeAttr: function() + { + var len; + if (!this.tidy.settings.removeAttr && this.tidy.settings.allowedAttr) + { - this.modalSetContent(content); - this.modalSetTitle(title); - this.modalSetDraggable(); - this.modalLoadTabs(); - this.modalOnCloseButton(); - this.modalSetButtonsWidth(); + var allowedAttrTags = [], allowedAttrData = []; + len = this.tidy.settings.allowedAttr.length; + for (var i = 0; i < len; i++) + { + allowedAttrTags.push(this.tidy.settings.allowedAttr[i][0]); + allowedAttrData.push(this.tidy.settings.allowedAttr[i][1]); + } - this.saveModalScroll = this.document.body.scrollTop; - if (this.opts.autoresize === false) - { - this.saveModalScroll = this.$editor.scrollTop(); - } - if (this.isMobile() === false) this.modalShowOnDesktop(); - else this.modalShowOnMobile(); + this.tidy.$div.find('*').each($.proxy(function(n,s) + { + var $el = $(s); + var pos = $.inArray($el[0].tagName.toLowerCase(), allowedAttrTags); + var attributesRemove = this.tidy.removeAttrGetRemoves(pos, allowedAttrData, $el); - // modal actions callback - if (typeof callback === 'function') - { - callback(); - } + if (attributesRemove) + { + $.each(attributesRemove, function(z,f) { + $el.removeAttr(f); + }); + } + }, this)); + } - // modal shown callback - setTimeout($.proxy(function() - { - this.callback('modalOpened', this.$redactorModal); + if (this.tidy.settings.removeAttr) + { + len = this.tidy.settings.removeAttr.length; + for (var i = 0; i < len; i++) + { + var attrs = this.tidy.settings.removeAttr[i][1]; + if ($.isArray(attrs)) attrs = attrs.join(' '); - }, this), 11); + this.tidy.$div.find(this.tidy.settings.removeAttr[i][0]).removeAttr(attrs); + } + } - // fix bootstrap modal focus - $(document).off('focusin.modal'); + }, + removeAttrGetRemoves: function(pos, allowed, $el) + { + var attributesRemove = []; - // enter - this.$redactorModal.find('input[type=text]').on('keypress', $.proxy(function(e) - { - if (e.which === 13) + // remove all attrs + if (pos == -1) + { + $.each($el[0].attributes, function(i, item) + { + attributesRemove.push(item.name); + }); + + } + // allow all attrs + else if (allowed[pos] == '*') + { + attributesRemove = []; + } + // allow specific attrs + else + { + $.each($el[0].attributes, function(i, item) + { + if ($.isArray(allowed[pos])) + { + if ($.inArray(item.name, allowed[pos]) == -1) + { + attributesRemove.push(item.name); + } + } + else if (allowed[pos] != item.name) + { + attributesRemove.push(item.name); + } + + }); + } + + return attributesRemove; + }, + removeAttrs: function (el, regex) { - this.$redactorModal.find('.redactor_modal_action_btn').click(); - e.preventDefault(); - } - }, this)); + regex = new RegExp(regex, "g"); + return el.each(function() + { + var self = $(this); + var len = this.attributes.length - 1; + for (var i = len; i >= 0; i--) + { + var item = this.attributes[i]; + if (item && item.specified && item.name.search(regex)>=0) + { + self.removeAttr(item.name); + } + } + }); + }, + removeEmpty: function() + { + if (!this.tidy.settings.removeEmpty) return; - return this.$redactorModal; + this.tidy.$div.find(this.tidy.settings.removeEmpty.join(',')).each(function() + { + var $el = $(this); + var text = $el.text(); + text = text.replace(/[\u200B-\u200D\uFEFF]/g, ''); + text = text.replace(/&nbsp;/gi, ''); + text = text.replace(/\s/g, ''); - }, - modalShowOnDesktop: function() - { - this.$redactorModal.css({ - position: 'fixed', - top: '-2000px', - left: '50%', - width: this.$redactorModalWidth + 'px', - marginLeft: '-' + (this.$redactorModalWidth / 2) + 'px' - }).show(); + if (text === '' && $el.children().length === 0) + { + $el.remove(); + } + }); + }, + removeParagraphsInLists: function() + { + this.tidy.$div.find('li p').contents().unwrap(); + }, + removeDataAttr: function() + { + if (!this.tidy.settings.removeDataAttr) return; - this.modalSaveBodyOveflow = $(document.body).css('overflow'); - $(document.body).css('overflow', 'hidden'); + var tags = this.tidy.settings.removeDataAttr; + if ($.isArray(this.tidy.settings.removeDataAttr)) tags = this.tidy.settings.removeDataAttr.join(','); - setTimeout($.proxy(function() - { - var height = this.$redactorModal.outerHeight(); - this.$redactorModal.css({ - top: '50%', - height: 'auto', - minHeight: 'auto', - marginTop: '-' + (height + 10) / 2 + 'px' - }); - }, this), 15); - }, - modalShowOnMobile: function() - { - this.$redactorModal.css({ - position: 'fixed', - width: '100%', - height: '100%', - top: '0', - left: '0', - margin: '0', - minHeight: '300px' - }).show(); - }, - modalSetContent: function(content) - { - this.modalcontent = false; - if (content.indexOf('#') == 0) - { - this.modalcontent = $(content); - $('#redactor_modal_inner').empty().append(this.modalcontent.html()); - this.modalcontent.html(''); + this.tidy.removeAttrs(this.tidy.$div.find(tags), '^(data-)'); - } - else - { - $('#redactor_modal_inner').empty().append(content); - } - }, - modalSetTitle: function(title) - { - this.$redactorModal.find('#redactor_modal_header').html(title); - }, - modalSetButtonsWidth: function() - { - var buttons = this.$redactorModal.find('footer button').not('.redactor_modal_btn_hidden'); - var buttonsSize = buttons.size(); - if (buttonsSize > 0) - { - $(buttons).css('width', (this.$redactorModalWidth/buttonsSize) + 'px') - } - }, - modalOnCloseButton: function() - { - this.$redactorModal.find('.redactor_btn_modal_close').on('click', $.proxy(this.modalClose, this)); - }, - modalSetOverlay: function() - { - if (this.opts.modalOverlay) - { - this.$redactorModalOverlay = $('#redactor_modal_overlay'); - if (!this.$redactorModalOverlay.length) + }, + removeWithoutAttr: function() { - this.$redactorModalOverlay = $('<div id="redactor_modal_overlay" style="display: none;"></div>'); - $('body').prepend(this.$redactorModalOverlay); - } + if (!this.tidy.settings.removeWithoutAttr) return; - this.$redactorModalOverlay.show().on('click', $.proxy(this.modalClose, this)); - } + this.tidy.$div.find(this.tidy.settings.removeWithoutAttr.join(',')).each(function() + { + if (this.attributes.length === 0) + { + $(this).contents().unwrap(); + } + }); + } + }; }, - modalSetDraggable: function() + toolbar: function() { - if (typeof $.fn.draggable !== 'undefined') - { - this.$redactorModal.draggable({ handle: '#redactor_modal_header' }); - this.$redactorModal.find('#redactor_modal_header').css('cursor', 'move'); - } - }, - modalLoadTabs: function() - { - var $redactor_tabs = $('#redactor_tabs'); - if (!$redactor_tabs.length) return false; - - var that = this; - $redactor_tabs.find('a').each(function(i, s) - { - i++; - $(s).on('click', function(e) + return { + init: function() { - e.preventDefault(); + return { + html: + { + title: this.lang.get('html'), + func: 'code.toggle' + }, + formatting: + { + title: this.lang.get('formatting'), + dropdown: + { + p: + { + title: this.lang.get('paragraph'), + func: 'block.format' + }, + blockquote: + { + title: this.lang.get('quote'), + func: 'block.format' + }, + pre: + { + title: this.lang.get('code'), + func: 'block.format' + }, + h1: + { + title: this.lang.get('header1'), + func: 'block.format' + }, + h2: + { + title: this.lang.get('header2'), + func: 'block.format' + }, + h3: + { + title: this.lang.get('header3'), + func: 'block.format' + }, + h4: + { + title: this.lang.get('header4'), + func: 'block.format' + }, + h5: + { + title: this.lang.get('header5'), + func: 'block.format' + } + } + }, + bold: + { + title: this.lang.get('bold'), + func: 'inline.format' + }, + italic: + { + title: this.lang.get('italic'), + func: 'inline.format' + }, + deleted: + { + title: this.lang.get('deleted'), + func: 'inline.format' + }, + underline: + { + title: this.lang.get('underline'), + func: 'inline.format' + }, + unorderedlist: + { + title: '&bull; ' + this.lang.get('unorderedlist'), + func: 'list.toggle' + }, + orderedlist: + { + title: '1. ' + this.lang.get('orderedlist'), + func: 'list.toggle' + }, + outdent: + { + title: '< ' + this.lang.get('outdent'), + func: 'indent.decrease' + }, + indent: + { + title: '> ' + this.lang.get('indent'), + func: 'indent.increase' + }, + image: + { + title: this.lang.get('image'), + func: 'image.show' + }, + file: + { + title: this.lang.get('file'), + func: 'file.show' + }, + link: + { + title: this.lang.get('link'), + dropdown: + { + link: + { + title: this.lang.get('link_insert'), + func: 'link.show' + }, + unlink: + { + title: this.lang.get('unlink'), + func: 'link.unlink' + } + } + }, + alignment: + { + title: this.lang.get('alignment'), + dropdown: + { + left: + { + title: this.lang.get('align_left'), + func: 'alignment.left' + }, + center: + { + title: this.lang.get('align_center'), + func: 'alignment.center' + }, + right: + { + title: this.lang.get('align_right'), + func: 'alignment.right' + }, + justify: + { + title: this.lang.get('align_justify'), + func: 'alignment.justify' + } + } + }, + horizontalrule: + { + title: this.lang.get('horizontalrule'), + func: 'line.insert' + } + }; + }, + build: function() + { + this.toolbar.hideButtons(); + this.toolbar.hideButtonsOnMobile(); + this.toolbar.isButtonSourceNeeded(); - $redactor_tabs.find('a').removeClass('redactor_tabs_act'); - $(this).addClass('redactor_tabs_act'); - $('.redactor_tab').hide(); - $('#redactor_tab' + i ).show(); - $('#redactor_tab_selected').val(i); + if (this.opts.buttons.length === 0) return; - if (that.isMobile() === false) + this.$toolbar = this.toolbar.createContainer(); + + this.toolbar.setOverflow(); + this.toolbar.append(); + this.toolbar.setFormattingTags(); + this.toolbar.loadButtons(); + this.toolbar.setFixed(); + + // buttons response + if (this.opts.activeButtons) { - var height = that.$redactorModal.outerHeight(); - that.$redactorModal.css('margin-top', '-' + (height + 10) / 2 + 'px'); + this.$editor.on('mouseup.redactor keyup.redactor focus.redactor', $.proxy(this.observe.buttons, this)); } - }); - }); - }, - modalCloseHandler: function(e) - { - if (e.keyCode === this.keyCode.ESC) - { - this.modalClose(); - return false; - } - }, - modalClose: function() - { - $('#redactor_modal_close').off('click', this.modalClose); - $('#redactor_modal').fadeOut('fast', $.proxy(function() - { - var redactorModalInner = $('#redactor_modal_inner'); + }, + createContainer: function() + { + return $('<ul>').addClass('redactor-toolbar').attr('id', 'redactor-toolbar-' + this.uuid); + }, + setFormattingTags: function() + { + $.each(this.opts.toolbar.formatting.dropdown, $.proxy(function (i, s) + { + if ($.inArray(i, this.opts.formatting) == -1) delete this.opts.toolbar.formatting.dropdown[i]; + }, this)); - if (this.modalcontent !== false) + }, + loadButtons: function() { - this.modalcontent.html(redactorModalInner.html()); - this.modalcontent = false; - } + $.each(this.opts.buttons, $.proxy(function(i, btnName) + { + if (!this.opts.toolbar[btnName]) return; - redactorModalInner.html(''); + if (this.opts.fileUpload === false && btnName === 'file') return true; + if ((this.opts.imageUpload === false && this.opts.s3 === false) && btnName === 'image') return true; - if (this.opts.modalOverlay) + var btnObject = this.opts.toolbar[btnName]; + this.$toolbar.append($('<li>').append(this.button.build(btnName, btnObject))); + + }, this)); + }, + append: function() { - $('#redactor_modal_overlay').hide().off('click', this.modalClose); - } + if (this.opts.toolbarExternal) + { + this.$toolbar.addClass('redactor-toolbar-external'); + $(this.opts.toolbarExternal).html(this.$toolbar); + } + else + { + this.$box.prepend(this.$toolbar); + } + }, + setFixed: function() + { + if (!this.utils.isDesktop()) return; + if (this.opts.toolbarExternal) return; + if (!this.opts.toolbarFixed) return; - $(document).off('keyup', this.modalCloseHandler); - this.$editor.off('keyup', this.modalCloseHandler); + this.toolbar.observeScroll(); + $(this.opts.toolbarFixedTarget).on('scroll.redactor', $.proxy(this.toolbar.observeScroll, this)); - this.selectionRestore(); - - // restore scroll - if (this.opts.autoresize && this.saveModalScroll) + }, + setOverflow: function() { - $(this.document.body).scrollTop(this.saveModalScroll); - } - else if (this.opts.autoresize === false && this.saveModalScroll) + if (this.utils.isMobile() && this.opts.toolbarOverflow) + { + this.$toolbar.addClass('redactor-toolbar-overflow'); + } + }, + isButtonSourceNeeded: function() { - this.$editor.scrollTop(this.saveModalScroll); - } + if (this.opts.buttonSource) return; - this.callback('modalClosed'); + var index = this.opts.buttons.indexOf('html'); + if (index !== -1) + { + this.opts.buttons.splice(index, 1); + } + }, + hideButtons: function() + { + if (this.opts.buttonsHide.length === 0) return; - }, this)); + $.each(this.opts.buttonsHide, $.proxy(function(i, s) + { + var index = this.opts.buttons.indexOf(s); + this.opts.buttons.splice(index, 1); + }, this)); + }, + hideButtonsOnMobile: function() + { + if (!this.utils.isMobile() || this.opts.buttonsHideOnMobile.length === 0) return; - if (this.isMobile() === false) - { - $(document.body).css('overflow', this.modalSaveBodyOveflow ? this.modalSaveBodyOveflow : 'visible'); - } + $.each(this.opts.buttonsHideOnMobile, $.proxy(function(i, s) + { + var index = this.opts.buttons.indexOf(s); + this.opts.buttons.splice(index, 1); - return false; - }, - modalSetTab: function(num) - { - $('.redactor_tab').hide(); - $('#redactor_tabs').find('a').removeClass('redactor_tabs_act').eq(num - 1).addClass('redactor_tabs_act'); - $('#redactor_tab' + num).show(); - }, + }, this)); + }, + observeScroll: function() + { + var scrollTop = $(this.opts.toolbarFixedTarget).scrollTop(); + var boxTop = 1; - // S3 - s3handleFileSelect: function(e) - { - var files = e.target.files; + if (this.opts.toolbarFixedTarget === document) + { + boxTop = this.$box.offset().top; + } - for (var i = 0, f; f = files[i]; i++) - { - this.s3uploadFile(f); - } - }, - s3uploadFile: function(file) - { - this.s3executeOnSignedUrl(file, $.proxy(function(signedURL) - { - this.s3uploadToS3(file, signedURL); - }, this)); - }, - s3executeOnSignedUrl: function(file, callback) - { - var xhr = new XMLHttpRequest(); + if (scrollTop > boxTop) + { + this.toolbar.observeScrollEnable(scrollTop, boxTop); + } + else + { + this.toolbar.observeScrollDisable(); + } + }, + observeScrollEnable: function(scrollTop, boxTop) + { + var top = this.opts.toolbarFixedTopOffset + scrollTop - boxTop; + var left = 0; + var end = boxTop + this.$box.height() + 30; + var width = this.$box.innerWidth(); - var mark = '?'; - if (this.opts.s3.search(/\?/) != '-1') mark = '&'; + this.$toolbar.addClass('toolbar-fixed-box'); + this.$toolbar.css({ + position: 'absolute', + width: width, + top: top + 'px', + left: left + }); - xhr.open('GET', this.opts.s3 + mark + 'name=' + file.name + '&type=' + file.type, true); + this.toolbar.setDropdownsFixed(); + this.$toolbar.css('visibility', (scrollTop < end) ? 'visible' : 'hidden'); + }, + observeScrollDisable: function() + { + this.$toolbar.css({ + position: 'relative', + width: 'auto', + top: 0, + left: 0, + visibility: 'visible' + }); - // Hack to pass bytes through unprocessed. - if (xhr.overrideMimeType) xhr.overrideMimeType('text/plain; charset=x-user-defined'); + this.toolbar.unsetDropdownsFixed(); + this.$toolbar.removeClass('toolbar-fixed-box'); - var that = this; - xhr.onreadystatechange = function(e) - { - if (this.readyState == 4 && this.status == 200) + }, + setDropdownsFixed: function() { - that.showProgressBar(); - callback(decodeURIComponent(this.responseText)); - } - else if(this.readyState == 4 && this.status != 200) + var top = this.$toolbar.innerHeight() + this.opts.toolbarFixedTopOffset; + var position = 'fixed'; + if (this.opts.toolbarFixedTarget !== document) + { + top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top) + this.opts.toolbarFixedTopOffset; + position = 'absolute'; + } + + $('.redactor-dropdown').each(function() + { + $(this).css({ position: position, top: top + 'px' }); + }); + }, + unsetDropdownsFixed: function() { - //setProgress(0, 'Could not contact signing script. Status = ' + this.status); + var top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top); + $('.redactor-dropdown').each(function() + { + $(this).css({ position: 'absolute', top: top + 'px' }); + }); } }; - - xhr.send(); }, - s3createCORSRequest: function(method, url) + upload: function() { - var xhr = new XMLHttpRequest(); - if ("withCredentials" in xhr) - { - xhr.open(method, url, true); - } - else if (typeof XDomainRequest != "undefined") - { - xhr = new XDomainRequest(); - xhr.open(method, url); - } - else - { - xhr = null; - } - - return xhr; - }, - s3uploadToS3: function(file, url) - { - var xhr = this.s3createCORSRequest('PUT', url); - if (!xhr) - { - //setProgress(0, 'CORS not supported'); - } - else - { - xhr.onload = $.proxy(function() + return { + init: function(id, url, callback) { - if (xhr.status == 200) - { - //setProgress(100, 'Upload completed.'); + this.upload.direct = false; + this.upload.callback = callback; + this.upload.url = url; + this.upload.$el = $(id); + this.upload.$droparea = $('<div id="redactor-droparea" />'); - this.hideProgressBar(); + this.upload.$placeholdler = $('<div id="redactor-droparea-placeholder" />').text('Drop file here or '); + this.upload.$input = $('<input type="file" name="file" />'); - var s3image = url.split('?'); + this.upload.$placeholdler.append(this.upload.$input); + this.upload.$droparea.append(this.upload.$placeholdler); + this.upload.$el.append(this.upload.$droparea); - if (!s3image[0]) - { - // url parsing is fail - return false; - } + this.upload.$droparea.off('redactor.upload'); + this.upload.$input.off('redactor.upload'); - this.selectionRestore(); + this.upload.$droparea.on('dragover.redactor.upload', $.proxy(this.upload.onDrag, this)); + this.upload.$droparea.on('dragleave.redactor.upload', $.proxy(this.upload.onDragLeave, this)); - var html = ''; - html = '<img id="image-marker" src="' + s3image[0] + '" />'; - if (this.opts.paragraphy) html = '<p>' + html + '</p>'; + // change + this.upload.$input.on('change.redactor.upload', $.proxy(function(e) + { + e = e.originalEvent || e; + this.upload.traverseFile(this.upload.$input[0].files[0], e); + }, this)); - this.execCommand('inserthtml', html, false); + // drop + this.upload.$droparea.on('drop.redactor.upload', $.proxy(function(e) + { + e.preventDefault(); - var image = $(this.$editor.find('img#image-marker')); + this.upload.$droparea.removeClass('drag-hover').addClass('drag-drop'); + this.upload.onDrop(e); - if (image.length) image.removeAttr('id'); - else image = false; + }, this)); + }, + directUpload: function(file, e) + { + this.upload.direct = true; + this.upload.traverseFile(file, e); + }, + onDrop: function(e) + { + e = e.originalEvent || e; + var files = e.dataTransfer.files; - this.sync(); + this.upload.traverseFile(files[0], e); + }, + traverseFile: function(file, e) + { + if (this.opts.s3) + { + this.upload.setConfig(file); + this.upload.s3uploadFile(file); + return; + } - // upload image callback - this.callback('imageUpload', image, false); + var formData = !!window.FormData ? new FormData() : null; + if (window.FormData) + { + this.upload.setConfig(file); - this.modalClose(); - this.observeImages(); + var name = (this.upload.type == 'image') ? this.opts.imageUploadParam : this.opts.fileUploadParam; + formData.append(name, file); + } + this.progress.show(); + this.upload.sendData(formData, e); + }, + setConfig: function(file) + { + this.upload.getType(file); + + if (this.upload.direct) + { + this.upload.url = (this.upload.type == 'image') ? this.opts.imageUpload : this.opts.fileUpload; + this.upload.callback = (this.upload.type == 'image') ? this.image.insert : this.file.insert; } - else + }, + getType: function(file) + { + this.upload.type = 'image'; + if (this.opts.imageTypes.indexOf(file.type) == -1) { - //setProgress(0, 'Upload error: ' + xhr.status); + this.upload.type = 'file'; } - }, this); - - xhr.onerror = function() + }, + getHiddenFields: function(obj, fd) { - //setProgress(0, 'XHR error.'); - }; + if (obj === false || typeof obj !== 'object') return fd; - xhr.upload.onprogress = function(e) + $.each(obj, $.proxy(function(k, v) + { + if (v !== null && v.toString().indexOf('#') === 0) v = $(v).val(); + fd.append(k, v); + + }, this)); + + return fd; + + }, + sendData: function(formData, e) { - /* - if (e.lengthComputable) + // append hidden fields + if (this.upload.type == 'image') { - var percentLoaded = Math.round((e.loaded / e.total) * 100); - setProgress(percentLoaded, percentLoaded == 100 ? 'Finalizing.' : 'Uploading.'); + formData = this.upload.getHiddenFields(this.opts.uploadImageFields, formData); + formData = this.upload.getHiddenFields(this.upload.imageFields, formData); } - */ - }; + else + { + formData = this.upload.getHiddenFields(this.opts.uploadFileFields, formData); + formData = this.upload.getHiddenFields(this.upload.fileFields, formData); + } - xhr.setRequestHeader('Content-Type', file.type); - xhr.setRequestHeader('x-amz-acl', 'public-read'); + var xhr = new XMLHttpRequest(); + xhr.open('POST', this.upload.url); - xhr.send(file); - } - }, + // complete + xhr.onreadystatechange = $.proxy(function() + { + if (xhr.readyState == 4) + { + var data = xhr.responseText; - // UPLOAD - uploadInit: function(el, options) - { - this.uploadOptions = { - url: false, - success: false, - error: false, - start: false, - trigger: false, - auto: false, - input: false - }; + data = data.replace(/^\[/, ''); + data = data.replace(/\]$/, ''); - $.extend(this.uploadOptions, options); + var json; + try + { + json = (typeof data === 'string' ? $.parseJSON(data) : data); + } + catch(err) + { + json = { + error: true + }; + } - var $el = $('#' + el); - // Test input or form - if ($el.length && $el[0].tagName === 'INPUT') - { - this.uploadOptions.input = $el; - this.el = $($el[0].form); - } - else this.el = $el; + this.progress.hide(); - this.element_action = this.el.attr('action'); + if (!this.upload.direct) + { + this.upload.$droparea.removeClass('drag-drop'); + } - // Auto or trigger - if (this.uploadOptions.auto) - { - $(this.uploadOptions.input).change($.proxy(function(e) - { - this.el.submit(function(e) + this.upload.callback(json, this.upload.direct, e); + } + }, this); + + + /* + xhr.upload.onprogress = $.proxy(function(e) { - return false; - }); + if (e.lengthComputable) + { + var complete = (e.loaded / e.total * 100 | 0); + //progress.value = progress.innerHTML = complete; + } - this.uploadSubmit(e); + }, this); + */ - }, this)); - } - else if (this.uploadOptions.trigger) - { - $('#' + this.uploadOptions.trigger).click($.proxy(this.uploadSubmit, this)); - } - }, - uploadSubmit: function(e) - { - this.showProgressBar(); - this.uploadForm(this.element, this.uploadFrame()); - }, - uploadFrame: function() - { - this.id = 'f' + Math.floor(Math.random() * 99999); + xhr.send(formData); + }, + onDrag: function(e) + { + e.preventDefault(); + this.upload.$droparea.addClass('drag-hover'); + }, + onDragLeave: function(e) + { + e.preventDefault(); + this.upload.$droparea.removeClass('drag-hover'); + }, + clearImageFields: function() + { + this.upload.imageFields = {}; + }, + addImageFields: function(name, value) + { + this.upload.imageFields[name] = value; + }, + removeImageFields: function(name) + { + delete this.upload.imageFields[name]; + }, + clearFileFields: function() + { + this.upload.fileFields = {}; + }, + addFileFields: function(name, value) + { + this.upload.fileFields[name] = value; + }, + removeFileFields: function(name) + { + delete this.upload.fileFields[name]; + }, - var d = this.document.createElement('div'); - var iframe = '<iframe style="display:none" id="' + this.id + '" name="' + this.id + '"></iframe>'; - d.innerHTML = iframe; - $(d).appendTo("body"); + // S3 + s3uploadFile: function(file) + { + this.upload.s3executeOnSignedUrl(file, $.proxy(function(signedURL) + { + this.upload.s3uploadToS3(file, signedURL); + }, this)); + }, + s3executeOnSignedUrl: function(file, callback) + { + var xhr = new XMLHttpRequest(); - // Start - if (this.uploadOptions.start) this.uploadOptions.start(); + var mark = '?'; + if (this.opts.s3.search(/\?/) != '-1') mark = '&'; - $( '#' + this.id ).load($.proxy(this.uploadLoaded, this)); + xhr.open('GET', this.opts.s3 + mark + 'name=' + file.name + '&type=' + file.type, true); - return this.id; - }, - uploadForm: function(f, name) - { - if (this.uploadOptions.input) - { - var formId = 'redactorUploadForm' + this.id, - fileId = 'redactorUploadFile' + this.id; + // Hack to pass bytes through unprocessed. + if (xhr.overrideMimeType) xhr.overrideMimeType('text/plain; charset=x-user-defined'); - this.form = $('<form action="' + this.uploadOptions.url + '" method="POST" target="' + name + '" name="' + formId + '" id="' + formId + '" enctype="multipart/form-data" />'); + var that = this; + xhr.onreadystatechange = function(e) + { + if (this.readyState == 4 && this.status == 200) + { + that.progress.show(); + callback(decodeURIComponent(this.responseText)); + } + else if (this.readyState == 4 && this.status != 200) + { + //setProgress(0, 'Could not contact signing script. Status = ' + this.status); + } + }; - // append hidden fields - if (this.opts.uploadFields !== false && typeof this.opts.uploadFields === 'object') + xhr.send(); + }, + s3createCORSRequest: function(method, url) { - $.each(this.opts.uploadFields, $.proxy(function(k, v) + var xhr = new XMLHttpRequest(); + if ("withCredentials" in xhr) { - if (v != null && v.toString().indexOf('#') === 0) v = $(v).val(); + xhr.open(method, url, true); + } + else if (typeof XDomainRequest != "undefined") + { + xhr = new XDomainRequest(); + xhr.open(method, url); + } + else + { + xhr = null; + } - var hidden = $('<input/>', { - 'type': "hidden", - 'name': k, - 'value': v - }); + return xhr; + }, + s3uploadToS3: function(file, url) + { + var xhr = this.upload.s3createCORSRequest('PUT', url); + if (!xhr) + { + //setProgress(0, 'CORS not supported'); + } + else + { + xhr.onload = $.proxy(function() + { + if (xhr.status == 200) + { + //setProgress(100, 'Upload completed.'); - $(this.form).append(hidden); + this.progress.hide(); - }, this)); - } + var s3file = url.split('?'); - var oldElement = this.uploadOptions.input; - var newElement = $(oldElement).clone(); + if (!s3file[0]) + { + // url parsing is fail + return false; + } - $(oldElement).attr('id', fileId).before(newElement).appendTo(this.form); - $(this.form).css('position', 'absolute') - .css('top', '-2000px') - .css('left', '-2000px') - .appendTo('body'); + if (!this.upload.direct) + { + this.upload.$droparea.removeClass('drag-drop'); + } - this.form.submit(); + var json = { filelink: s3file[0] }; + if (this.upload.type == 'file') + { + var arr = s3file[0].split('/'); + json.filename = arr[arr.length-1]; + } - } - else - { - f.attr('target', name) - .attr('method', 'POST') - .attr('enctype', 'multipart/form-data') - .attr('action', this.uploadOptions.url); + this.upload.callback(json, this.upload.direct, false); - this.element.submit(); - } - }, - uploadLoaded: function() - { - var i = $( '#' + this.id)[0], d; - if (i.contentDocument) d = i.contentDocument; - else if (i.contentWindow) d = i.contentWindow.document; - else d = window.frames[this.id].document; + } + else + { + //setProgress(0, 'Upload error: ' + xhr.status); + } + }, this); - // Success - if (this.uploadOptions.success) - { - this.hideProgressBar(); + xhr.onerror = function() + { + //setProgress(0, 'XHR error.'); + }; - if (typeof d !== 'undefined') - { - // Remove bizarre <pre> tag wrappers around our json data: - var rawString = d.body.innerHTML; - var jsonString = rawString.match(/\{(.|\n)*\}/)[0]; + xhr.upload.onprogress = function(e) + { + /* + if (e.lengthComputable) + { + var percentLoaded = Math.round((e.loaded / e.total) * 100); + setProgress(percentLoaded, percentLoaded == 100 ? 'Finalizing.' : 'Uploading.'); + } + */ + }; - jsonString = jsonString.replace(/^\[/, ''); - jsonString = jsonString.replace(/\]$/, ''); + xhr.setRequestHeader('Content-Type', file.type); + xhr.setRequestHeader('x-amz-acl', 'public-read'); - var json = $.parseJSON(jsonString); - - if (typeof json.error == 'undefined') this.uploadOptions.success(json); - else - { - this.uploadOptions.error(this, json); - this.modalClose(); + xhr.send(file); } } - else - { - this.modalClose(); - alert('Upload failed!'); - } - } - - this.el.attr('action', this.element_action); - this.el.attr('target', ''); + }; }, - - // DRAGUPLOAD - draguploadInit: function (el, options) + utils: function() { - this.draguploadOptions = $.extend({ - url: false, - success: false, - error: false, - preview: false, - uploadFields: false, - text: this.opts.curLang.drop_file_here, - atext: this.opts.curLang.or_choose, - uploadParam: false - }, options); + return { + isMobile: function() + { + return /(iPhone|iPod|BlackBerry|Android)/.test(navigator.userAgent); + }, + isDesktop: function() + { + return !/(iPhone|iPod|iPad|BlackBerry|Android)/.test(navigator.userAgent); + }, + isString: function(obj) + { + return Object.prototype.toString.call(obj) == '[object String]'; + }, + isEmpty: function(html, removeEmptyTags) + { + html = html.replace(/[\u200B-\u200D\uFEFF]/g, ''); + html = html.replace(/&nbsp;/gi, ''); + html = html.replace(/<\/?br\s?\/?>/g, ''); + html = html.replace(/\s/g, ''); + html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, ''); + html = html.replace(/<iframe(.*?[^>])>$/i, 'iframe'); - if (window.FormData === undefined) return false; + // remove empty tags + if (removeEmptyTags !== false) + { + html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, ''); + html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, ''); + } - this.droparea = $('<div class="redactor_droparea"></div>'); - this.dropareabox = $('<div class="redactor_dropareabox">' + this.draguploadOptions.text + '</div>'); - this.dropalternative = $('<div class="redactor_dropalternative">' + this.draguploadOptions.atext + '</div>'); + html = $.trim(html); - this.droparea.append(this.dropareabox); + return html === ''; + }, + normalize: function(str) + { + if (typeof(str) === 'undefined') return 0; + return parseInt(str.replace('px',''), 10); + }, + hexToRgb: function(hex) + { + if (typeof hex == 'undefined') return; + if (hex.search(/^#/) == -1) return hex; - $(el).before(this.droparea); - $(el).before(this.dropalternative); + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function(m, r, g, b) + { + return r + r + g + g + b + b; + }); - // drag over - this.dropareabox.on('dragover', $.proxy(function() - { - return this.draguploadOndrag(); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return 'rgb(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ')'; + }, + getOuterHtml: function(el) + { + return $('<div>').append($(el).eq(0).clone()).html(); + }, + getAlignmentElement: function(el) + { + if ($.inArray(el.tagName, this.opts.alignmentTags) !== -1) + { + return $(el); + } + else + { + return $(el).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]); + } + }, + removeEmptyAttr: function(el, attr) + { + var $el = $(el); + if (typeof $el.attr(attr) == 'undefined') + { + return true; + } - }, this)); + if ($el.attr(attr) === '') + { + $el.removeAttr(attr); + return true; + } - // drag leave - this.dropareabox.on('dragleave', $.proxy(function() - { - return this.draguploadOndragleave(); + return false; + }, + removeEmpty: function(i, s) + { + var $s = $(s); - }, this)); + $s.find('.redactor-invisible-space').removeAttr('style').removeAttr('class'); - // drop - this.dropareabox.get(0).ondrop = $.proxy(function(e) - { - e.preventDefault(); + if ($s.find('hr, br, img, iframe').length !== 0) return; + var text = $.trim($s.text()); + if (this.utils.isEmpty(text, false)) + { + $s.remove(); + } + }, - this.dropareabox.removeClass('hover').addClass('drop'); - this.showProgressBar(); - this.dragUploadAjax(this.draguploadOptions.url, e.dataTransfer.files[0], false, e, this.draguploadOptions.uploadParam); - - }, this ); - }, - dragUploadAjax: function(url, file, directupload, e, uploadParam) - { - if (!directupload) - { - var xhr = $.ajaxSettings.xhr(); - if (xhr.upload) + // save and restore scroll + saveScroll: function() { - xhr.upload.addEventListener('progress', $.proxy(this.uploadProgress, this), false); - } + if (this.utils.isSelectAll()) return; - $.ajaxSetup({ - xhr: function () { return xhr; } - }); - } + this.saveEditorScroll = this.$editor.scrollTop(); + this.saveBodyScroll = $(window).scrollTop(); - // drop callback - this.callback('drop', e); + if (this.opts.scrollTarget) this.saveTargetScroll = $(this.opts.scrollTarget).scrollTop(); + }, + restoreScroll: function() + { + if (typeof this.saveScroll === 'undefined' && typeof this.saveBodyScroll === 'undefined') return; - var fd = new FormData(); + $(window).scrollTop(this.saveBodyScroll); + this.$editor.scrollTop(this.saveEditorScroll); - // append file data - if (uploadParam !== false) - { - fd.append(uploadParam, file); - } - else - { - fd.append('file', file); - } + if (this.opts.scrollTarget) $(this.opts.scrollTarget).scrollTop(this.saveTargetScroll); + }, - // append hidden fields - if (this.opts.uploadFields !== false && typeof this.opts.uploadFields === 'object') - { - $.each(this.opts.uploadFields, $.proxy(function(k, v) + // get invisible space element + createSpaceElement: function() { - if (v != null && v.toString().indexOf('#') === 0) v = $(v).val(); - fd.append(k, v); + var space = document.createElement('span'); + space.className = 'redactor-invisible-space'; + space.innerHTML = this.opts.invisibleSpace; - }, this)); - } + return space; + }, - $.ajax({ - url: url, - dataType: 'html', - data: fd, - cache: false, - contentType: false, - processData: false, - type: 'POST', - success: $.proxy(function(data) + // replace + removeInlineTags: function(node) { - data = data.replace(/^\[/, ''); - data = data.replace(/\]$/, ''); + var tags = this.opts.inlineTags; + tags.push('span'); - var json = (typeof data === 'string' ? $.parseJSON(data) : data); + if (node.tagName == 'PRE') tags.push('a'); - this.hideProgressBar(); + $(node).find(tags.join(',')).not('span.redactor-selection-marker').contents().unwrap(); + }, + replaceWithContents: function(node, removeInlineTags) + { + var self = this; + $(node).replaceWith(function() + { + if (removeInlineTags === true) self.utils.removeInlineTags(this); - if (directupload) + return $(this).contents(); + }); + }, + replaceToTag: function(node, tag, removeInlineTags) + { + var replacement; + var self = this; + $(node).replaceWith(function() { - var $img = $('<img>'); - $img.attr('src', json.filelink).attr('id', 'drag-image-marker'); + replacement = $('<' + tag + ' />').append($(this).contents()); - this.insertNodeToCaretPositionFromPoint(e, $img[0]); + for (var i = 0; i < this.attributes.length; i++) + { + replacement.attr(this.attributes[i].name, this.attributes[i].value); + } - var image = $(this.$editor.find('img#drag-image-marker')); - if (image.length) image.removeAttr('id'); - else image = false; + if (removeInlineTags === true) self.utils.removeInlineTags(replacement); - this.sync(); - this.observeImages(); + return replacement; + }); - // upload callback - if (image) this.callback('imageUpload', image, json); + return replacement; + }, - // error callback - if (typeof json.error !== 'undefined') this.callback('imageUploadError', json); - } - else - { - if (typeof json.error == 'undefined') - { - this.draguploadOptions.success(json); - } - else - { - this.draguploadOptions.error(this, json); - this.draguploadOptions.success(false); - } - } + // start and end of element + isStartOfElement: function() + { + var block = this.selection.getBlock(); + if (!block) return false; - }, this) - }); - }, - draguploadOndrag: function() - { - this.dropareabox.addClass('hover'); - return false; - }, - draguploadOndragleave: function() - { - this.dropareabox.removeClass('hover'); - return false; - }, - uploadProgress: function(e, text) - { - var percent = e.loaded ? parseInt(e.loaded / e.total * 100, 10) : e; - this.dropareabox.text('Loading ' + percent + '% ' + (text || '')); - }, + var offset = this.caret.getOffsetOfElement(block); - // UTILS - isMobile: function() - { - return /(iPhone|iPod|BlackBerry|Android)/.test(navigator.userAgent); - }, - isIPad: function() - { - return /iPad/.test(navigator.userAgent); - }, - normalize: function(str) - { - if (typeof(str) === 'undefined') return 0; - return parseInt(str.replace('px',''), 10); - }, - outerHtml: function(el) - { - return $('<div>').append($(el).eq(0).clone()).html(); - }, - stripHtml: function(html) - { - var tmp = document.createElement("DIV"); - tmp.innerHTML = html; - return tmp.textContent || tmp.innerText || ""; - }, - isString: function(obj) - { - return Object.prototype.toString.call(obj) == '[object String]'; - }, - isEmpty: function(html) - { - html = html.replace(/&#x200b;|<br>|<br\/>|&nbsp;/gi, ''); - html = html.replace(/\s/g, ''); - html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, ''); + return (offset === 0) ? true : false; + }, + isEndOfElement: function() + { + var block = this.selection.getBlock(); + if (!block) return false; - return html == ''; - }, - getInternetExplorerVersion: function() - { - var rv = false; - if (navigator.appName == 'Microsoft Internet Explorer') - { - var ua = navigator.userAgent; - var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); - if (re.exec(ua) != null) + var offset = this.caret.getOffsetOfElement(block); + var text = $.trim($(block).text()).replace(/\n\r\n/g, ''); + + return (offset == text.length) ? true : false; + }, + + // test blocks + isBlock: function(block) { - rv = parseFloat(RegExp.$1); - } - } + block = block[0] || block; - return rv; - }, - isIe11: function() - { - return !!navigator.userAgent.match(/Trident\/7\./); - }, - browser: function(browser) - { - var ua = navigator.userAgent.toLowerCase(); - var match = /(opr)[\/]([\w.]+)/.exec( ua ) || - /(chrome)[ \/]([\w.]+)/.exec( ua ) || - /(webkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec( ua ) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || - /(msie) ([\w.]+)/.exec( ua ) || - ua.indexOf("trident") >= 0 && /(rv)(?::| )([\w.]+)/.exec( ua ) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || - []; + return block && this.utils.isBlockTag(block.tagName); + }, + isBlockTag: function(tag) + { + if (typeof tag == 'undefined') return false; - if (browser == 'version') return match[2]; - if (browser == 'webkit') return (match[1] == 'chrome' || match[1] == 'webkit'); - if (match[1] == 'rv') return browser == 'msie'; - if (match[1] == 'opr') return browser == 'webkit'; + return this.reIsBlock.test(tag); + }, - return browser == match[1]; + // tag detection + isTag: function(current, tag) + { + var element = $(current).closest(tag); + if (element.size() == 1) + { + return element[0]; + } - }, - oldIE: function() - { - if (this.browser('msie') && parseInt(this.browser('version'), 10) < 9) return true; - return false; - }, - getFragmentHtml: function (fragment) - { - var cloned = fragment.cloneNode(true); - var div = this.document.createElement('div'); + return false; + }, - div.appendChild(cloned); - return div.innerHTML; - }, - extractContent: function() - { - var node = this.$editor[0]; - var frag = this.document.createDocumentFragment(); - var child; + // select all + isSelectAll: function() + { + return this.selectAll; + }, + enableSelectAll: function() + { + this.selectAll = true; + }, + disableSelectAll: function() + { + this.selectAll = false; + }, - while ((child = node.firstChild)) - { - frag.appendChild(child); - } + // parents detection + isRedactorParent: function(el) + { + if (!el) + { + return false; + } - return frag; - }, - isParentRedactor: function(el) - { - if (!el) return false; - if (this.opts.iframe) return el; + if ($(el).parents('.redactor-editor').length === 0 || $(el).hasClass('redactor-editor')) + { + return false; + } - if ($(el).parents('div.redactor_editor').length == 0 || $(el).hasClass('redactor_editor')) return false; - else return el; - }, - currentOrParentIs: function(tagName) - { - var parent = this.getParent(), current = this.getCurrent(); - return parent && parent.tagName === tagName ? parent : current && current.tagName === tagName ? current : false; - }, - isEndOfElement: function() - { - var current = this.getBlock(); - var offset = this.getCaretOffset(current); + return el; + }, + isCurrentOrParentHeader: function() + { + return this.utils.isCurrentOrParent(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']); + }, + isCurrentOrParent: function(tagName) + { + var parent = this.selection.getParent(); + var current = this.selection.getCurrent(); - var text = $.trim($(current).text()).replace(/\n\r\n/g, ''); + if ($.isArray(tagName)) + { + var matched = 0; + $.each(tagName, $.proxy(function(i, s) + { + if (this.utils.isCurrentOrParentOne(current, parent, s)) + { + matched++; + } + }, this)); - var len = text.length; + return (matched === 0) ? false : true; + } + else + { + return this.utils.isCurrentOrParentOne(current, parent, tagName); + } + }, + isCurrentOrParentOne: function(current, parent, tagName) + { + tagName = tagName.toUpperCase(); - if (offset == len) return true; - else return false; - }, - isFocused: function() - { - var el, sel = this.getSelection(); + return parent && parent.tagName === tagName ? parent : current && current.tagName === tagName ? current : false; + }, - if (sel && sel.rangeCount && sel.rangeCount > 0) el = sel.getRangeAt(0).startContainer; - if (!el) return false; - if (this.opts.iframe) - { - if (this.getCaretOffsetRange().equals()) return !this.$editor.is(el); - else return true; - } - return $(el).closest('div.redactor_editor').length != 0; - }, - removeEmptyAttr: function (el, attr) - { - if ($(el).attr(attr) == '') $(el).removeAttr(attr); - }, - removeFromArrayByValue: function(array, value) - { - var index = null; + // browsers detection + isOldIe: function() + { + return (this.utils.browser('msie') && parseInt(this.utils.browser('version'), 10) < 9) ? true : false; + }, + isLessIe10: function() + { + return (this.utils.browser('msie') && parseInt(this.utils.browser('version'), 10) < 10) ? true : false; + }, + isIe11: function() + { + return !!navigator.userAgent.match(/Trident\/7\./); + }, + browser: function(browser) + { + var ua = navigator.userAgent.toLowerCase(); + var match = /(opr)[\/]([\w.]+)/.exec( ua ) || + /(chrome)[ \/]([\w.]+)/.exec( ua ) || + /(webkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec( ua ) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || + /(msie) ([\w.]+)/.exec( ua ) || + ua.indexOf("trident") >= 0 && /(rv)(?::| )([\w.]+)/.exec( ua ) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || + []; - while ((index = array.indexOf(value)) !== -1) - { - array.splice(index, 1); - } + if (browser == 'version') return match[2]; + if (browser == 'webkit') return (match[1] == 'chrome' || match[1] == 'webkit'); + if (match[1] == 'rv') return browser == 'msie'; + if (match[1] == 'opr') return browser == 'webkit'; - return array; + return browser == match[1]; + } + }; } - }; // constructor Redactor.prototype.init.prototype = Redactor.prototype; // LINKIFY - $.Redactor.fn.formatLinkify = function(protocol, convertLinks, convertImageLinks, convertVideoLinks, linkSize) + $.Redactor.fn.formatLinkify = function(protocol, convertLinks, convertUrlLinks, convertImageLinks, convertVideoLinks, linkSize) { - var url = /(((https?|ftps?):\/\/)|www[.][^\s])(.+?\..+?)([.),]?)(\s|\.\s+|\)|$)/gi, - rProtocol = /(https?|ftp):\/\//i, - urlImage = /(https?:\/\/.*\.(?:png|jpg|jpeg|gif))/gi; + 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.get(0) : this).childNodes, i = childNodes.length; + var childNodes = (this.$editor ? this.$editor[0] : this).childNodes, i = childNodes.length; while (i--) { var n = childNodes[i]; - if (n.nodeType === 3) + + if (n.nodeType === 3 && n.parentNode !== 'PRE') { var html = n.nodeValue; // youtube & vimeo if (convertVideoLinks && html) @@ -8249,49 +8172,49 @@ } // image if (convertImageLinks && html && html.match(urlImage)) { - html = html.replace(urlImage, '<img src="$1">'); + html = html.replace(urlImage, '<img src="$1" />'); $(n).after(html).remove(); + return; } // link - if (convertLinks && html && html.match(url)) + if (html.search(/\$/g) != -1) html = html.replace(/\$/g, '&#36;'); + + var matches = html.match(regex); + if (convertUrlLinks && html && matches) { - var matches = html.match(url); - for (var i in matches) + var len = matches.length; + for (var z = 0; z < len; z++) { - var href = matches[i]; + // 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; + 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;'); - text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') - - /* - To handle URLs which may have $ characters in them, need to escape $ -> $$ to prevent $1 from getting treated as a backreference. - See http://gotofritz.net/blog/code-snippets/escaping-in-replace-strings-in-javascript/ - */ - var escapedBackReferences = text.replace('$', '$$$'); - - html = html.replace(href, '<a href=\"' + addProtocol + $.trim(href) + '\">' + $.trim(escapedBackReferences) + '</a>' + space); + html = html.replace(href, '<a href=\"' + addProtocol + $.trim(href) + '\">' + $.trim(text) + '</a>' + space); } $(n).after(html).remove(); } } - else if (n.nodeType === 1 && !/^(a|button|textarea)$/i.test(n.tagName)) + else if (n.nodeType === 1 && !/^(pre|a|button|textarea)$/i.test(n.tagName)) { - $.Redactor.fn.formatLinkify.call(n, protocol, convertLinks, convertImageLinks, convertVideoLinks, linkSize); + $.Redactor.fn.formatLinkify.call(n, protocol, convertLinks, convertUrlLinks, convertImageLinks, convertVideoLinks, linkSize); } } }; })(jQuery); \ No newline at end of file