UNPKG

nodebb-plugin-composer-redactor

Version:

Redactor Composer for NodeBB

2,140 lines (1,841 loc) 292 kB
/* Redactor II Version 2.11 Updated: September 20, 2017 http://imperavi.com/redactor/ Copyright (c) 2009-2017, Imperavi LLC. License: http://imperavi.com/redactor/license/ Usage: $('#content').redactor(); */ (function($) { 'use strict'; if (!Function.prototype.bind) { Function.prototype.bind = function(scope) { var fn = this; return function() { return fn.apply(scope); }; }; } var uuid = 0; // Plugin $.fn.redactor = function(options) { var val = []; var args = Array.prototype.slice.call(arguments, 1); if (typeof options === 'string') { this.each(function() { var instance = $.data(this, 'redactor'); var func; if (options.search(/\./) !== '-1') { func = options.split('.'); if (typeof instance[func[0]] !== 'undefined') { func = instance[func[0]][func[1]]; } } 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() { $.data(this, 'redactor', {}); $.data(this, 'redactor', Redactor(this, options)); }); } if (val.length === 0) { return this; } else if (val.length === 1) { return val[0]; } else { return val; } }; // Initialization function Redactor(el, options) { return new Redactor.prototype.init(el, options); } // Options $.Redactor = Redactor; $.Redactor.VERSION = '2.11'; $.Redactor.modules = ['air', 'autosave', 'block', 'buffer', 'build', 'button', 'caret', 'clean', 'code', 'core', 'detect', 'dropdown', 'events', 'file', 'focus', 'image', 'indent', 'inline', 'insert', 'keydown', 'keyup', 'lang', 'line', 'link', 'linkify', 'list', 'marker', 'modal', 'observe', 'offset', 'paragraphize', 'paste', 'placeholder', 'progress', 'selection', 'shortcuts', 'storage', 'toolbar', 'upload', 'uploads3', 'utils', 'browser' // deprecated ]; $.Redactor.settings = {}; $.Redactor.opts = { // settings animation: false, lang: 'en', direction: 'ltr', spellcheck: true, overrideStyles: true, stylesClass: false, scrollTarget: document, focus: false, focusEnd: false, clickToEdit: false, structure: false, tabindex: false, minHeight: false, // string maxHeight: false, // string maxWidth: false, // string plugins: false, // array callbacks: {}, placeholder: false, linkify: true, enterKey: true, pastePlainText: false, pasteImages: true, pasteLinks: true, pasteBlockTags: ['pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'ul', 'ol', 'li', 'blockquote', 'p', 'figure', 'figcaption'], pasteInlineTags: ['br', 'strong', 'ins', 'code', 'del', 'span', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small', 'b', 'u', 'em', 'i'], preClass: false, // string preSpaces: 4, // or false tabAsSpaces: false, // true or number of spaces tabKey: true, autosave: false, // false or url autosaveName: false, autosaveFields: false, imageUpload: null, imageUploadParam: 'file', imageUploadFields: false, imageUploadForms: false, imageTag: 'figure', imageEditable: true, imageCaption: true, imagePosition: false, imageResizable: false, imageFloatMargin: '10px', dragImageUpload: true, multipleImageUpload: true, clipboardImageUpload: true, fileUpload: null, fileUploadParam: 'file', fileUploadFields: false, fileUploadForms: false, dragFileUpload: true, s3: false, linkNewTab: false, linkTooltip: true, linkNofollow: false, linkSize: 30, linkValidation: true, pasteLinkTarget: false, videoContainerClass: 'video-container', toolbar: true, toolbarFixed: true, toolbarFixedTarget: document, toolbarFixedTopOffset: 0, // pixels toolbarExternal: false, // ID selector toolbarOverflow: false, air: false, airWidth: false, formatting: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], formattingAdd: false, buttons: ['format', 'bold', 'italic', 'deleted', 'lists', 'image', 'file', 'link', 'horizontalrule'], // + 'horizontalrule', 'underline', 'ol', 'ul', 'indent', 'outdent' buttonsTextLabeled: false, buttonsHide: [], buttonsHideOnMobile: [], script: true, removeNewlines: false, removeComments: true, replaceTags: { 'b': 'strong', 'i': 'em', 'strike': 'del' }, keepStyleAttr: [], // tag name array keepInlineOnEnter: false, // shortcuts 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, activeButtons: ['deleted', 'italic', 'bold'], activeButtonsStates: { b: 'bold', strong: 'bold', i: 'italic', em: 'italic', del: 'deleted', strike: 'deleted' }, // private lang langs: { en: { "format": "Format", "image": "Image", "file": "File", "link": "Link", "bold": "Bold", "italic": "Italic", "deleted": "Strikethrough", "underline": "Underline", "bold-abbr": "B", "italic-abbr": "I", "deleted-abbr": "S", "underline-abbr": "U", "lists": "Lists", "link-insert": "Insert link", "link-edit": "Edit link", "link-in-new-tab": "Open link in new tab", "unlink": "Unlink", "cancel": "Cancel", "close": "Close", "insert": "Insert", "save": "Save", "delete": "Delete", "text": "Text", "edit": "Edit", "title": "Title", "paragraph": "Normal text", "quote": "Quote", "code": "Code", "heading1": "Heading 1", "heading2": "Heading 2", "heading3": "Heading 3", "heading4": "Heading 4", "heading5": "Heading 5", "heading6": "Heading 6", "filename": "Name", "optional": "optional", "unorderedlist": "Unordered List", "orderedlist": "Ordered List", "outdent": "Outdent", "indent": "Indent", "horizontalrule": "Line", "upload-label": "Drop file here or ", "caption": "Caption", "bulletslist": "Bullets", "numberslist": "Numbers", "image-position": "Position", "none": "None", "left": "Left", "right": "Right", "center": "Center", "accessibility-help-label": "Rich text editor" } }, // private type: 'textarea', // textarea, div, inline, pre inline: false, inlineTags: ['a', 'span', 'strong', 'strike', 'b', 'u', 'em', 'i', 'code', 'del', 'ins', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small'], blockTags: ['pre', 'ul', 'ol', 'li', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'dt', 'dd', 'div', 'td', 'blockquote', 'output', 'figcaption', 'figure', 'address', 'section', 'header', 'footer', 'aside', 'article', 'iframe'], paragraphize: true, paragraphizeBlocks: ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption', 'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea', 'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p'], emptyHtml: '<p>&#x200b;</p>', invisibleSpace: '&#x200b;', emptyHtmlRendered: $('').html('​').html(), imageTypes: ['image/png', 'image/jpeg', 'image/gif'], userAgent: navigator.userAgent.toLowerCase(), observe: { dropdowns: [] }, regexps: { linkyoutube: /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.\-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig, linkvimeo: /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/, linkimage: /((https?|www)[^\s]+\.)(jpe?g|png|gif)(\?[^\s-]+)?/ig, url: /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/ig } }; // Functionality Redactor.fn = $.Redactor.prototype = { keyCode: { BACKSPACE: 8, DELETE: 46, UP: 38, 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 }, // =init init: function(el, options) { this.$element = $(el); this.uuid = uuid++; this.sBuffer = []; this.sRebuffer = []; this.loadOptions(options); this.loadModules(); // click to edit if (this.opts.clickToEdit && !this.$element.hasClass('redactor-click-to-edit')) { return this.loadToEdit(options); } else if (this.$element.hasClass('redactor-click-to-edit')) { this.$element.removeClass('redactor-click-to-edit'); } // block & inline test tag regexp this.reIsBlock = new RegExp('^(' + this.opts.blockTags.join('|' ).toUpperCase() + ')$', 'i'); this.reIsInline = new RegExp('^(' + this.opts.inlineTags.join('|' ).toUpperCase() + ')$', 'i'); // set up drag upload this.opts.dragImageUpload = (this.opts.imageUpload === null) ? false : this.opts.dragImageUpload; this.opts.dragFileUpload = (this.opts.fileUpload === null) ? false : this.opts.dragFileUpload; // formatting storage this.formatting = {}; // load lang this.lang.load(); // extend shortcuts $.extend(this.opts.shortcuts, this.opts.shortcutsAdd); // set editor this.$editor = this.$element; // detect type of editor this.detectType(); // start callback this.core.callback('start'); this.core.callback('startToEdit'); // build this.start = true; this.build.start(); }, detectType: function() { if (this.build.isInline() || this.opts.inline) { this.opts.type = 'inline'; } else if (this.build.isTag('DIV')) { this.opts.type = 'div'; } else if (this.build.isTag('PRE')) { this.opts.type = 'pre'; } }, loadToEdit: function(options) { this.$element.on('click.redactor-click-to-edit', $.proxy(function() { this.initToEdit(options); }, this)); this.$element.addClass('redactor-click-to-edit'); return; }, initToEdit: function(options) { $.extend(options.callbacks, { startToEdit: function() { this.insert.node(this.marker.get(), false); }, initToEdit: function() { this.selection.restore(); this.clickToCancelStorage = this.code.get(); // cancel $(this.opts.clickToCancel).off('.redactor-click-to-edit'); $(this.opts.clickToCancel).show().on('click.redactor-click-to-edit', $.proxy(function(e) { e.preventDefault(); this.core.destroy(); this.events.syncFire = false; this.$element.html(this.clickToCancelStorage); this.core.callback('cancel', this.clickToCancelStorage); this.events.syncFire = true; this.clickToCancelStorage = ''; $(this.opts.clickToCancel).hide(); $(this.opts.clickToSave).hide(); this.$element.on('click.redactor-click-to-edit', $.proxy(function() { this.initToEdit(options); }, this)); this.$element.addClass('redactor-click-to-edit'); }, this)); // save $(this.opts.clickToSave).off('.redactor-click-to-edit'); $(this.opts.clickToSave).show().on('click.redactor-click-to-edit', $.proxy(function(e) { e.preventDefault(); this.core.destroy(); this.core.callback('save', this.code.get()); $(this.opts.clickToCancel).hide(); $(this.opts.clickToSave).hide(); this.$element.on('click.redactor-click-to-edit', $.proxy(function() { this.initToEdit(options); }, this)); this.$element.addClass('redactor-click-to-edit'); }, this)); } }); this.$element.redactor(options); this.$element.off('.redactor-click-to-edit'); }, loadOptions: function(options) { var settings = {}; // check namespace if (typeof $.Redactor.settings.namespace !== 'undefined') { if (this.$element.hasClass($.Redactor.settings.namespace)) { settings = $.Redactor.settings; } } else { settings = $.Redactor.settings; } this.opts = $.extend( {}, $.Redactor.opts, this.$element.data(), options ); this.opts = $.extend({}, this.opts, settings); }, getModuleMethods: function(object) { return Object.getOwnPropertyNames(object).filter(function(property) { return typeof object[property] === 'function'; }); }, loadModules: function() { var len = $.Redactor.modules.length; for (var i = 0; i < len; i++) { this.bindModuleMethods($.Redactor.modules[i]); } }, bindModuleMethods: function(module) { if (typeof this[module] === 'undefined') { return; } // init module this[module] = this[module](); var methods = this.getModuleMethods(this[module]); var len = methods.length; // bind methods for (var z = 0; z < len; z++) { this[module][methods[z]] = this[module][methods[z]].bind(this); } }, // =air air: function() { return { enabled: false, collapsed: function() { if (this.opts.air) { this.selection.get().collapseToStart(); } }, collapsedEnd: function() { if (this.opts.air) { this.selection.get().collapseToEnd(); } }, build: function() { if (this.detect.isMobile()) { return; } this.button.hideButtons(); this.button.hideButtonsOnMobile(); if (this.opts.buttons.length === 0) { return; } this.$air = this.air.createContainer(); if (this.opts.airWidth !== false) { this.$air.css('width', this.opts.airWidth); } this.air.append(); this.button.$toolbar = this.$air; this.button.setFormatting(); this.button.load(this.$air); this.core.editor().on('mouseup.redactor', this, $.proxy(function(e) { if (this.selection.text() !== '') { this.air.show(e); } }, this)); }, append: function() { this.$air.appendTo('body'); }, createContainer: function() { return $('<ul>').addClass('redactor-air').attr({ 'id': 'redactor-air-' + this.uuid, 'role': 'toolbar' }).hide(); }, show: function (e) { //this.marker.remove(); this.selection.saveInstant(); //this.selection.restore(false); $('.redactor-air').hide(); var leftFix = 0; var width = this.$air.innerWidth(); if ($(window).width() < (e.clientX + width)) { leftFix = 200; } this.$air.css({ left: (e.clientX - leftFix) + 'px', top: (e.clientY + 10 + $(document).scrollTop()) + 'px' }).show(); this.air.enabled = true; this.air.bindHide(); }, bindHide: function() { $(document).on('mousedown.redactor-air.' + this.uuid, $.proxy(function(e) { var dropdown = $(e.target).closest('.redactor-dropdown').length; if ($(e.target).closest(this.$air).length === 0 && dropdown === 0) { var hide = this.air.hide(e); if (hide !== false) { this.marker.remove(); } } }, this)).on('keydown.redactor-air.' + this.uuid, $.proxy(function(e) { var key = e.which; if ((!this.utils.isRedactorParent(e.target) && !$(e.target).hasClass('redactor-in')) || $(e.target).closest('#redactor-modal').length !== 0) { return; } if (key === this.keyCode.ESC) { this.selection.get().collapseToStart(); //this.marker.remove(); } else if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE) { var sel = this.selection.get(); var range = this.selection.range(sel); range.deleteContents(); //this.marker.remove(); } else if (key === this.keyCode.ENTER) { this.selection.get().collapseToEnd(); //this.marker.remove(); } if (this.air.enabled) { this.air.hide(e); } else { this.selection.get().collapseToStart(); //this.marker.remove(); } }, this)); }, hide: function(e) { var ctrl = e.ctrlKey || e.metaKey || (e.shiftKey && e.altKey); if (ctrl) { return false; } this.button.setInactiveAll(); this.$air.fadeOut(100); this.air.enabled = false; $(document).off('mousedown.redactor-air.' + this.uuid); $(document).off('keydown.redactor-air.' + this.uuid); } }; }, // =autosave autosave: function() { return { enabled: false, html: false, init: function() { if (!this.opts.autosave) { return; } this.autosave.enabled = true; this.autosave.name = (this.opts.autosaveName) ? this.opts.autosaveName : this.$textarea.attr('name'); }, is: function() { return this.autosave.enabled; }, send: function() { if (!this.opts.autosave) { return; } this.autosave.source = this.code.get(); if (this.autosave.html === this.autosave.source) { return; } // data var data = {}; data.name = this.autosave.name; data[this.autosave.name] = this.autosave.source; data = this.autosave.getHiddenFields(data); // ajax var jsxhr = $.ajax({ url: this.opts.autosave, type: 'post', data: data }); jsxhr.done(this.autosave.success); }, getHiddenFields: function(data) { if (this.opts.autosaveFields === false || typeof this.opts.autosaveFields !== 'object') { return data; } $.each(this.opts.autosaveFields, $.proxy(function(k, v) { if (v !== null && v.toString().indexOf('#') === 0) { v = $(v).val(); } data[k] = v; }, this)); return data; }, success: function(data) { var json; try { json = JSON.parse(data); } catch(e) { //data has already been parsed json = data; } var callbackName = (typeof json.error === 'undefined') ? 'autosave' : 'autosaveError'; this.core.callback(callbackName, this.autosave.name, json); this.autosave.html = this.autosave.source; }, disable: function() { this.autosave.enabled = false; clearInterval(this.autosaveTimeout); } }; }, // =block block: function() { return { format: function(tag, attr, value, type) { tag = (tag === 'quote') ? 'blockquote' : tag; this.block.tags = ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'figure']; if ($.inArray(tag, this.block.tags) === -1) { return; } if (tag === 'p' && typeof attr === 'undefined') { // remove all attr = 'class'; } this.placeholder.hide(); this.buffer.set(); return (this.utils.isCollapsed()) ? this.block.formatCollapsed(tag, attr, value, type) : this.block.formatUncollapsed(tag, attr, value, type); }, formatCollapsed: function(tag, attr, value, type) { this.selection.save(); var block = this.selection.block(); var currentTag = block.tagName.toLowerCase(); if ($.inArray(currentTag, this.block.tags) === -1) { this.selection.restore(); return; } var clearAllAttrs = false; if (currentTag === tag && attr === undefined) { tag = 'p'; clearAllAttrs = true; } if (clearAllAttrs) { this.block.removeAllClass(); this.block.removeAllAttr(); } var replaced; if (currentTag === 'blockquote' && this.utils.isEndOfElement(block)) { this.marker.remove(); replaced = document.createElement('p'); replaced.innerHTML = this.opts.invisibleSpace; $(block).after(replaced); this.caret.start(replaced); var $last = $(block).children().last(); if ($last.length !== 0 && $last[0].tagName === 'BR') { $last.remove(); } } else { replaced = this.utils.replaceToTag(block, tag); } if (typeof attr === 'object') { type = value; for (var key in attr) { replaced = this.block.setAttr(replaced, key, attr[key], type); } } else { replaced = this.block.setAttr(replaced, attr, value, type); } // trim pre if (tag === 'pre' && replaced.length === 1) { $(replaced).html($.trim($(replaced).html())); } this.selection.restore(); this.block.removeInlineTags(replaced); return replaced; }, formatUncollapsed: function(tag, attr, value, type) { this.selection.save(); var replaced = []; var blocks = this.selection.blocks(); if (blocks[0] && ($(blocks[0]).hasClass('redactor-in') || $(blocks[0]).hasClass('redactor-box'))) { blocks = this.core.editor().find(this.opts.blockTags.join(', ')); } var len = blocks.length; for (var i = 0; i < len; i++) { var currentTag = blocks[i].tagName.toLowerCase(); if ($.inArray(currentTag, this.block.tags) !== -1 && currentTag !== 'figure') { var block = this.utils.replaceToTag(blocks[i], tag); if (typeof attr === 'object') { type = value; for (var key in attr) { block = this.block.setAttr(block, key, attr[key], type); } } else { block = this.block.setAttr(block, attr, value, type); } replaced.push(block); this.block.removeInlineTags(block); } } this.selection.restore(); // combine pre if (tag === 'pre' && replaced.length !== 0) { var first = replaced[0]; $.each(replaced, function(i,s) { if (i !== 0) { $(first).append("\n" + $.trim(s.html())); $(s).remove(); } }); replaced = []; replaced.push(first); } return replaced; }, removeInlineTags: function(node) { node = node[0] || node; var tags = this.opts.inlineTags; var blocks = ['PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6']; if ($.inArray(node.tagName, blocks) === - 1) { return; } if (node.tagName !== 'PRE') { var index = tags.indexOf('a'); tags.splice(index, 1); } $(node).find(tags.join(',')).not('.redactor-selection-marker').contents().unwrap(); }, setAttr: function(block, attr, value, type) { if (typeof attr === 'undefined') { return block; } var func = (typeof type === 'undefined') ? 'replace' : type; if (attr === 'class') { block = this.block[func + 'Class'](value, block); } else { if (func === 'remove') { block = this.block[func + 'Attr'](attr, block); } else if (func === 'removeAll') { block = this.block[func + 'Attr'](attr, block); } else { block = this.block[func + 'Attr'](attr, value, block); } } return block; }, getBlocks: function(block) { block = (typeof block === 'undefined') ? this.selection.blocks() : block; if ($(block).hasClass('redactor-box')) { var blocks = []; var nodes = this.core.editor().children(); $.each(nodes, $.proxy(function(i,node) { if (this.utils.isBlock(node)) { blocks.push(node); } }, this)); return blocks; } return block }, replaceClass: function(value, block) { return $(this.block.getBlocks(block)).removeAttr('class').addClass(value)[0]; }, toggleClass: function(value, block) { return $(this.block.getBlocks(block)).toggleClass(value)[0]; }, addClass: function(value, block) { return $(this.block.getBlocks(block)).addClass(value)[0]; }, removeClass: function(value, block) { return $(this.block.getBlocks(block)).removeClass(value)[0]; }, removeAllClass: function(block) { return $(this.block.getBlocks(block)).removeAttr('class')[0]; }, replaceAttr: function(attr, value, block) { block = this.block.removeAttr(attr, block); return $(block).attr(attr, value)[0]; }, toggleAttr: function(attr, value, block) { block = this.block.getBlocks(block); var self = this; var returned = []; $.each(block, function(i,s) { var $el = $(s); if ($el.attr(attr)) { returned.push(self.block.removeAttr(attr, s)); } else { returned.push(self.block.addAttr(attr, value, s)); } }); return returned; }, addAttr: function(attr, value, block) { return $(this.block.getBlocks(block)).attr(attr, value)[0]; }, removeAttr: function(attr, block) { return $(this.block.getBlocks(block)).removeAttr(attr)[0]; }, removeAllAttr: function(block) { block = this.block.getBlocks(block); var returned = []; $.each(block, function(i,s) { if (typeof s.attributes !== 'undefined') { while (s.attributes.length) { s.removeAttribute(s.attributes[0].name); } } returned.push(s); }); return returned; } }; }, // buffer buffer: function() { return { set: function(type) { if (typeof type === 'undefined') { this.buffer.clear(); } if (typeof type === 'undefined' || type === 'undo') { this.buffer.setUndo(); } else { this.buffer.setRedo(); } }, setUndo: function() { var saved = this.selection.saveInstant(); var last = this.sBuffer[this.sBuffer.length-1]; var current = this.core.editor().html(); var save = (typeof last !== 'undefined' && (last[0] === current)) ? false : true; if (save) { this.sBuffer.push([current, saved]); } //this.selection.restore(); }, setRedo: function() { var saved = this.selection.saveInstant(); this.sRebuffer.push([this.core.editor().html(), saved]); //this.selection.restore(); }, add: function() { this.sBuffer.push([this.core.editor().html(), 0]); }, undo: function() { if (this.sBuffer.length === 0) { return; } var buffer = this.sBuffer.pop(); this.buffer.set('redo'); this.core.editor().html(buffer[0]); this.selection.restoreInstant(buffer[1]); this.selection.restore(); this.observe.load(); }, redo: function() { if (this.sRebuffer.length === 0) { return; } var buffer = this.sRebuffer.pop(); this.buffer.set('undo'); this.core.editor().html(buffer[0]); this.selection.restoreInstant(buffer[1]); this.selection.restore(); this.observe.load(); }, clear: function() { this.sRebuffer = []; } }; }, // =build build: function() { return { start: function() { if (this.opts.type === 'inline') { this.opts.type = 'inline'; } else if (this.opts.type === 'div') { // empty var html = $.trim(this.$editor.html()); if (html === '') { this.$editor.html(this.opts.emptyHtml); } this.build.buildTextarea(); } else if (this.opts.type === 'textarea') { this.build.startTextarea(); } // set in this.build.setIn(); // set id this.build.setId(); // enable this.build.enableEditor(); // options this.build.setOptions(); // call this.build.callEditor(); }, createContainerBox: function() { this.$box = $('<div class="redactor-box" role="application" />'); }, setIn: function() { this.core.editor().addClass('redactor-in'); }, setId: function() { var id = (this.opts.type === 'textarea') ? 'redactor-uuid-' + this.uuid : this.$element.attr('id'); this.core.editor().attr('id', (typeof id === 'undefined') ? 'redactor-uuid-' + this.uuid : id); }, getName: function() { var name = this.$element.attr('name'); return (typeof name === 'undefined') ? 'content-' + this.uuid : name; }, buildTextarea: function() { this.$textarea = $('<textarea>'); this.$textarea.attr('name', this.build.getName()); this.$textarea.hide(); this.$element.after(this.$textarea); this.build.setStartAttrs(); }, loadFromTextarea: function() { this.$editor = $('<div />'); // textarea this.$textarea = this.$element; this.$element.attr('name', this.build.getName()); // place this.$box.insertAfter(this.$element).append(this.$editor).append(this.$element); this.build.setStartAttrs(); // styles this.$editor.addClass('redactor-layer'); if (this.opts.overrideStyles) this.$editor.addClass('redactor-styles'); this.$element.hide(); this.$box.prepend('<span class="redactor-voice-label" id="redactor-voice-' + this.uuid +'" aria-hidden="false">' + this.lang.get('accessibility-help-label') + '</span>'); }, setStartAttrs: function() { this.$editor.attr({ 'aria-labelledby': 'redactor-voice-' + this.uuid, 'role': 'presentation' }); }, startTextarea: function() { this.build.createContainerBox(); // load this.build.loadFromTextarea(); // set code this.code.start(this.core.textarea().val()); // set value this.core.textarea().val(this.clean.onSync(this.$editor.html())); }, isTag: function(tag) { return (this.$element[0].tagName === tag); }, isInline: function() { return (!this.build.isTag('TEXTAREA') && !this.build.isTag('DIV') && !this.build.isTag('PRE')); }, enableEditor: function() { this.core.editor().attr({ 'contenteditable': true }); }, setOptions: function() { // inline if (this.opts.type === 'inline') { this.opts.enterKey = false; } // inline & pre if (this.opts.type === 'inline' || this.opts.type === 'pre') { this.opts.toolbarMobile = false; this.opts.toolbar = false; this.opts.air = false; this.opts.linkify = false; } // spellcheck this.core.editor().attr('spellcheck', this.opts.spellcheck); // structure if (this.opts.structure) { this.core.editor().addClass('redactor-structure'); } // styles class if (this.opts.stylesClass) { this.core.editor().addClass(this.opts.stylesClass); } // options sets only in textarea mode if (this.opts.type !== 'textarea') { return; } // direction this.core.box().attr('dir', this.opts.direction); this.core.editor().attr('dir', this.opts.direction); // tabindex if (this.opts.tabindex) { this.core.editor().attr('tabindex', this.opts.tabindex); } // min height if (this.opts.minHeight) { this.core.editor().css('min-height', this.opts.minHeight); } else { this.core.editor().css('min-height', '40px'); } // max height if (this.opts.maxHeight) { this.core.editor().css('max-height', this.opts.maxHeight); } // max width if (this.opts.maxWidth) { this.core.editor().css({ 'max-width': this.opts.maxWidth, 'margin': 'auto' }); } }, callEditor: function() { this.build.disableBrowsersEditing(); this.events.init(); this.build.setHelpers(); // init buttons if (this.opts.toolbar || this.opts.air) { this.toolbarsButtons = this.button.init(); } // load toolbar if (this.opts.air) { this.air.build(); } else if (this.opts.toolbar) { this.toolbar.build(); } if (this.detect.isMobile() && this.opts.toolbarMobile && this.opts.air) { this.opts.toolbar = true; this.toolbar.build(); } // observe dropdowns if (this.opts.air || this.opts.toolbar) { this.core.editor().on('mouseup.redactor-observe.' + this.uuid + ' keyup.redactor-observe.' + this.uuid + ' focus.redactor-observe.' + this.uuid + ' touchstart.redactor-observe.' + this.uuid, $.proxy(this.observe.toolbar, this)); this.core.element().on('blur.callback.redactor', $.proxy(function() { this.button.setInactiveAll(); }, this)); } // modal templates init this.modal.templates(); // plugins this.build.plugins(); // autosave this.autosave.init(); // sync code this.code.html = this.code.cleaned(this.core.editor().html()); // init callback this.core.callback('init'); this.core.callback('initToEdit'); // get images & files list this.storage.observe(); // started this.start = false; }, setHelpers: function() { // linkify if (this.opts.linkify) { this.linkify.format(); } // placeholder this.placeholder.init(); // focus if (this.opts.focus) { setTimeout(this.focus.start, 100); } else if (this.opts.focusEnd) { setTimeout(this.focus.end, 100); } }, disableBrowsersEditing: function() { try { // FF fix document.execCommand('enableObjectResizing', false, false); document.execCommand('enableInlineTableEditing', false, false); // IE prevent converting links document.execCommand("AutoUrlDetect", false, false); } catch (e) {} }, plugins: function() { if (!this.opts.plugins) { return; } $.each(this.opts.plugins, $.proxy(function(i, s) { var func = (typeof RedactorPlugins !== 'undefined' && typeof RedactorPlugins[s] !== 'undefined') ? RedactorPlugins : Redactor.fn; if (!$.isFunction(func[s])) { return; } this[s] = func[s](); // get methods var methods = this.getModuleMethods(this[s]); var len = methods.length; // bind methods for (var z = 0; z < len; z++) { this[s][methods[z]] = this[s][methods[z]].bind(this); } // append lang if (typeof this[s].langs !== 'undefined') { var lang = {}; if (typeof this[s].langs[this.opts.lang] !== 'undefined') { lang = this[s].langs[this.opts.lang]; } else if (typeof this[s].langs[this.opts.lang] === 'undefined' && typeof this[s].langs.en !== 'undefined') { lang = this[s].langs.en; } // extend var self = this; $.each(lang, function(i,s) { if (typeof self.opts.curLang[i] === 'undefined') { self.opts.curLang[i] = s; } }); } // init if ($.isFunction(this[s].init)) { this[s].init(); } }, this)); } }; }, // =button button: function() { return { toolbar: function() { return (typeof this.button.$toolbar === 'undefined' || !this.button.$toolbar) ? this.$toolbar : this.button.$toolbar; }, init: function() { return { format: { title: this.lang.get('format'), icon: true, 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('heading1'), func: 'block.format' }, h2: { title: this.lang.get('heading2'), func: 'block.format' }, h3: { title: this.lang.get('heading3'), func: 'block.format' }, h4: { title: this.lang.get('heading4'), func: 'block.format' }, h5: { title: this.lang.get('heading5'), func: 'block.format' }, h6: { title: this.lang.get('heading6'), func: 'block.format' } } }, bold: { title: this.lang.get('bold-abbr'), icon: true, label: this.lang.get('bold'), func: 'inline.format' }, italic: { title: this.lang.get('italic-abbr'), icon: true, label: this.lang.get('italic'), func: 'inline.format' }, deleted: { title: this.lang.get('deleted-abbr'), icon: true, label: this.lang.get('deleted'), func: 'inline.format' }, underline: { title: this.lang.get('underline-abbr'), icon: true, label: this.lang.get('underline'), func: 'inline.format' }, lists: { title: this.lang.get('lists'), icon: true, dropdown: { 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', observe: { element: 'li', out: { attr: { 'class': 'redactor-dropdown-link-inactive', 'aria-disabled': true } } } }, indent: { title: '> ' + this.lang.get('indent'), func: 'indent.increase', observe: { element: 'li', out: { attr: { 'class': 'redactor-dropdown-link-inactive', 'aria-disabled': true } } } } } }, ul: { title: '&bull; ' + this.lang.get('bulletslist'), icon: true, func: 'list.toggle' }, ol: { title: '1. ' + this.lang.get('numberslist'), icon: true, func: 'list.toggle' }, outdent: { title: this.lang.get('outdent'), icon: true, func: 'indent.decrease' }, indent: { title: this.lang.get('indent'), icon: true, func: 'indent.increase' }, image: { title: this.lang.get('image'), icon: true, func: 'image.show' }, file: { title: this.lang.get('file'), icon: true, func: 'file.show' }, link: { title: this.lang.get('link'), icon: true, dropdown: { link: { title: this.lang.get('link-insert'), func: 'link.show', observe: { element: 'a', 'in': { title: this.lang.get('link-edit') }, out: { title: this.lang.get('link-insert') } } }, unlink: { title: this.lang.get('unlink'), func: 'link.unlink', observe: { element: 'a', out: { attr: { 'class': 'redactor-dropdown-link-inactive', 'aria-disabled': true } } } } } }, horizontalrule: { title: this.lang.get('horizontalrule'), icon: true, func: 'line.insert' } }; }, setFormatting: function() { $.each(this.toolbarsButtons.format.dropdown, $.proxy(function (i, s) { if ($.inArray(i, this.opts.formatting) === -1) { delete this.toolbarsButtons.format.dropdown[i]; } }, this)); }, hideButtons: function() { if (this.opts.buttonsHide.length !== 0) { this.button.hideButtonsSlicer(this.opts.buttonsHide); } }, hideButtonsOnMobile: function() { if (this.detect.isMobile() && this.opts.buttonsHideOnMobile.length !== 0) { this.button.hideButtonsSlicer(this.opts.buttonsHideOnMobile); } }, hideButtonsSlicer: function(buttons) { $.each(buttons, $.proxy(function(i, s) { var index = this.opts.buttons.indexOf(s); if (index !== -1) { this.opts.buttons.splice(index, 1); } }, this)); }, load: function($toolbar) { this.button.buttons = []; $.each(this.opts.buttons, $.proxy(function(i, btnName) { if (!this.toolbarsButtons[btnName] || (btnName === 'file' && !this.file.is()) || (btnName === 'image' && !this.image.is())) { return; } $toolbar.append($('<li>').append(this.button.build(btnName, this.toolbarsButtons[btnName]))); }, this)); }, buildButtonTooltip: function($btn, title) { if (this.opts.air || this.detect.isMobile()) { return; } var $tooltip = $('<span>'); $tooltip.addClass('re-button-tooltip'); $tooltip.html(title); var self = this; var $toolbar = this.button.toolbar(); var $box = $toolbar.closest('.redactor-toolbar-box'); $box = ($box.length === 0) ? $toolbar : $box; $box.prepend($tooltip); $btn.on('mouseover', function() { if ($(this).hasClass('redactor-button-disabled')) { return; } var pos = ($toolbar.hasClass('toolbar-fixed-box')) ? $btn.offset() : $btn.position(); pos = (self.opts.toolbarFixedTarget !== document) ? $btn.position() : pos; var top = ($toolbar.hasClass('toolbar-fixed-box')) ? $btn.position().top : pos.top; var height = $btn.innerHeight(); var width = $btn.innerWidth(); var posBox = ($toolbar.hasClass('toolbar-fixed-box')) ? 'fixed' : 'absolute'; posBox = (self.opts.toolbarFixedTarget !== document) ? 'absolute' : posBox; var scrollFix = (self.opts.toolbarFixedTarget !== document) ? $toolbar.position().top : 0; $tooltip.show(); $tooltip.css({ top: (top + height + scrollFix) + 'px', left: (pos.left + width/2 - $tooltip.innerWidth()/2) + 'px', position: posBox }); }).on('mouseout', function() { $tooltip.hide(); }); }, build: function(btnName, btnObject) { if (this.opts.toolbar === false) { return; } var title = (typeof btnObject.label !== 'undefined') ? btnObject.label : btnObject.title; var $button = $('<a href="javascript:void(null);" alt="' + title + '" rel="' + btnName + '" />') $button.addClass('re-button re-' + btnName); $button.attr({ 'role': 'button', 'aria-label': title, 'tabindex': '-1' }); if (typeof btnObject.icon !== 'undefined' && !this.opts.buttonsTextLabeled) { var $icon = $('<i>'); $icon.addClass('re-icon-' + btnName); $button.append($icon); $button.addClass('re-button-icon'); this.button.buildButtonTooltip($button, title); } else { $button.html(btnObject.title); } // click if (btnObject.func || btnObject.command || btnObject.dropdown) { this.button.setEvent($button, btnName, btnObject); } // dropdown if (btnObject.dropdown) { $button.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true); var $dropdown = $('<ul class="redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-box-' + btnName + '" style="display: none;">'); $button.data('dropdown', $dropdown); this.dropdown.build(btnName, $dropdown, btnObject.dropdown); } this.button.buttons.push($button); return $button; }, getButtons: function() { return this.button.toolbar().find('a.re-button'); }, getButtonsKeys: function() { return this.button.buttons; }, setEvent: function($button, btnName, btnObject) { $button.on('mousedown', $.proxy(function(e) { e.preventDefault(); if ($button.hasClass('redactor-button-disabled')) { return false; } var type = 'func'; var callback = btnObject.func; if (btnObject.command) { type = 'command'; callback = btnObject.command; } else if (btnObject.dropdown) { type = 'dropdown'; callback = false; } this.button.toggle(e, btnName, type, callback); return false; }, this)); }, toggle: function(e, btnName, type, callback, args) { if (this.detect.isIe() || !this.detect.isDesktop()) { this.utils.freezeScroll(); e.returnValue = false; } if (type === 'command') { this.inline.format(callback); } else if (type === 'dropdown') { this.dropdown.show(e, btnName); } else { this.button.clickCallback(e, callback, btnName, args); } if (type !== 'dropdown') { this.dropdown.hideAll(false); } if (this.opts.air && type !== 'dropdown') { this.air.hide(e); } if (this.detect.isIe() || !this.detect.isDesktop()) { this.utils.unfreezeScroll(); } }, clickCallback: function(e, callback, btnName, args) { var func; args = (typeof args === 'undefined') ? btnName : args; if ($.isFunction(callback)) { callback.call(this, btnName); } else if (callback.search(/\./) !== '-1') { func = callback.split('.'); if (typeof this[func[0]] === 'undefined') { return; } if (typeof args === 'object') { this[func[0]][func[1]].apply(this, args); } else { this[func[0]][func[1]].call(this, args); } } else { if (typeof args === 'object') { this[callback].apply(this, args); } else { this[callback].call(this, args); } } this.observe.buttons(e, btnName); }, all: function() { return this.button.buttons; }, get: function(key) { if (this.opts.toolbar === false) { return; } return this.button.toolbar().find('a.re-' + key); }, set: function(key, title) { if (this.opts.toolbar === false) { return; } var $btn = this.button.toolbar().find('a.re-' + key); $btn.html(title).attr('aria-label', title); return $btn; }, add: function(key, title) { if (this.button.isAdded(key) !== true) { return $(); } var btn = this.button.build(key, { title: title }); this.button.toolbar().append($('<li>').append(btn)); return btn; }, addFirst: function(key, title) { if (this.button.isAdded(key) !== true) { return $(); } var btn = this.button.build(key, { title: title }); this.button.toolbar().prepend($('<li>').append(btn)); return btn; }, addAfter: function(afterkey, key, title) { if (this.button.isAdded(key) !== true) { return $(); } var btn = this.button.build(key, { title: title }); var $btn = this.button.get(afterkey); if ($btn.length !== 0) { $btn.parent().after($('<li>').append(btn)); } else { this.button.toolbar().append($('<li>').append(btn)); } return btn; }, addBefore: function(beforekey, key, title) { if (this.button.isAdded(key) !== true) { return $(); } var btn = this.button.build(key, { title: title }); var $btn = this.button.get(beforekey); if ($btn.length !== 0) { $btn.parent().before($('<li>').append(btn)); } else { thi