nodebb-plugin-composer-redactor
Version:
Redactor Composer for NodeBB
2,140 lines (1,841 loc) • 292 kB
JavaScript
/*
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>​</p>',
invisibleSpace: '​',
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: '• ' + 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: '• ' + 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