The biggest change was the transition to use <p> instead of <br> (disabled linebreaks mode), because attempts to style a lonely textnode (with no actual parent element except the editor container) causes a lot of issues.
Furthermore I have started to modify the HTML of Redactor's modal overlays, they (partially) mimic the appearance of WCF's overlays.
var $autosave = $textarea.data('autosave');
var $config = {
buttons: $buttons,
- linebreaks: true,
minHeight: 200,
- plugins: [ 'wutil', 'wmonkeypatch', 'wbutton', 'wbbcode', 'wfontcolor' ],
+ plugins: [ 'wutil', 'wmonkeypatch', 'wbutton', 'wbbcode', 'wfontcolor', 'wfontfamily', 'wfontsize' ],
wautosave: {
active: ($autosave) ? true : false,
key: ($autosave) ? $autosave : '',
'{@$__wcf->getPath()}js/3rdParty/redactor/plugins/wbbcode.js',
'{@$__wcf->getPath()}js/3rdParty/redactor/plugins/wbutton.js',
'{@$__wcf->getPath()}js/3rdParty/redactor/plugins/wfontcolor.js',
+ '{@$__wcf->getPath()}js/3rdParty/redactor/plugins/wfontfamily.js',
+ '{@$__wcf->getPath()}js/3rdParty/redactor/plugins/wfontsize.js',
'{@$__wcf->getPath()}js/3rdParty/redactor/plugins/wmonkeypatch.js',
'{@$__wcf->getPath()}js/3rdParty/redactor/plugins/wutil.js'
{event name='javascriptFiles'}
*/
toggle: function(direct) {
if (this.opts.visual) {
+ this._convertParagraphs();
this.toggleCode(direct);
this._convertFromHtml();
return $string;
},
+ _convertParagraphs: function() {
+ this.$editor.find('p').replaceWith(function() {
+ var $html = $(this).html();
+ if ($html == '<br>') {
+ // an empty line is presented by <p><br></p> but in the textarea this equals only a single new line
+ return $html;
+ }
+
+ return $html + '<br>';;
+ });
+ this.sync();
+ },
+
/**
* Converts source contents from HTML into BBCode.
*/
_convertFromHtml: function() {
var html = this.$source.val();
- if (html == '<br>' || html == '<p><br></p>') {
- return "";
- }
-
- // Convert <br> to line breaks.
- html = html.replace(/<br><\/p>/gi,"\n");
- html = html.replace(/<br(?=[ \/>]).*?>/gi, '\r\n');
- html = html.replace(/<p>/gi,"");
- html = html.replace(/<\/p>/gi,"\n");
+ // drop <br>, they are pointless because the editor already adds a newline after them
+ html = html.replace(/<br>/g, '');
html = html.replace(/ /gi," ");
// [email]
html = html.replace(/<span style="font-size: ?(\d+)pt;?">([\s\S]*?)<\/span>/gi, "[size=$1]$2[/size]");
// [font]
- html = html.replace(/<span style="font-family: ?(.*?);?">([\s\S]*?)<\/span>/gi, "[font='$1']$2[/font]");
+ html = html.replace(/<span style="font-family: ?(.*?);?">([\s\S]*?)<\/span>/gi, function(match, fontFamily, text) {
+ return "[font='" + fontFamily.replace(/'/g, '') + "']" + text + "[/font]";
+ });
// [align]
html = html.replace(/<div style="text-align: ?(left|center|right|justify);? ?">([\s\S]*?)<\/div>/gi, "[align=$1]$2[/align]");
// Restore %20
html = html.replace(/%20/g, ' ');
+ // trim leading tabs
+ var $tmp = html.split("\n");
+ for (var $i = 0, $length = $tmp.length; $i < $length; $i++) {
+ $tmp[$i] = $tmp[$i].replace(/^\s*/, '');
+ }
+ html = $tmp.join("\n");
+
this.$source.val(html);
},
data = data.replace(/>/g, '>');
//}
- // Convert line breaks to <br>.
- data = data.replace(/(?:\r\n|\n|\r)/g, '<br>');
-
/*if ($pasted) {
$pasted = false;
// skip
// remove "javascript:"
data = data.replace(/(javascript):/gi, '$1<span></span>:');
+ // unify line breaks
+ data = data.replace(/(\r|\r\n)/, "\n");
+
+ // convert line breaks into <p></p> or empty lines to <p><br></p>
+ var $tmp = data.split("\n");
+ data = '';
+ for (var $i = 0, $length = $tmp.length; $i < $length; $i++) {
+ var $line = $.trim($tmp[$i]);
+ if (!$line) {
+ $line = '<br>';
+ }
+
+ data += '<p>' + $line + '</p>';
+ }
+
// insert codes
if ($.getLength($cachedCodes)) {
for (var $key in $cachedCodes) {
this._addBBCodeButton(__REDACTOR_BUTTONS[$i]);
}
+ // this list contains overrides for built-in buttons, if a button is not present
+ // Redactor's own icon will be used instead. This solves the problem of FontAwesome
+ // not providing an icon for everything we need (especially the core stuff)
var $faIcons = {
- 'alignment': 'align-left',
- 'deleted': 'strikethrough',
- 'html': 'square-o',
- 'image': 'picture-o',
- 'orderedlist': 'list-ol',
- 'smiley': 'smile-o',
- 'unorderedlist': 'list-ul'
+ 'html': 'fa-square-o',
+ 'bold': 'fa-bold',
+ 'italic': 'fa-italic',
+ 'underline': 'fa-underline',
+ 'deleted': 'fa-strikethrough',
+ 'subscript': 'fa-subscript',
+ 'superscript': 'fa-superscript',
+ 'orderedlist': 'fa-list-ol',
+ 'unorderedlist': 'fa-list-ul',
+ 'outdent': 'fa-outdent',
+ 'indent': 'fa-indent',
+ 'link': 'fa-link',
+ 'alignment': 'fa-align-left',
+ 'table': 'fa-table'
};
var $buttons = this.getOption('buttons');
// check if button does not exist
var $buttonObj = this.buttonGet($button);
- var $className = ($faIcons[$button]) ? $faIcons[$button] : $button;
if ($buttonObj.length) {
- this.buttonAwesome($button, 'fa-' + $className);
+ if ($faIcons[$button]) {
+ this.buttonAwesome($button, $faIcons[$button]);
+ }
}
else {
- this._addCoreButton($button, $className, $lastButton);
+ this._addCoreButton($button, ($faIcons[$button] ? $faIcons[$button] : null), $lastButton);
}
$lastButton = $button;
}
},
- _addCoreButton: function(buttonName, className, insertAfter) {
+ _addCoreButton: function(buttonName, faIcon, insertAfter) {
var $button = this.buttonBuild(buttonName, {
title: buttonName,
exec: buttonName
}, false);
$('<li />').append($button).insertAfter(this.buttonGet(insertAfter).parent());
- this.buttonAwesome(buttonName, 'fa-' + className);
+ if (faIcon !== null) {
+ this.buttonAwesome(buttonName, faIcon);
+ }
},
/**
init: function() {
this._createFontColorDropdown();
- this.buttonAdd('fontcolor', this.opts.curLang.fontcolor, $.proxy(function(btnName, $button, btnObject, e) {
+ this.buttonReplace('fontcolor', 'fontcolor', this.opts.curLang.fontcolor, $.proxy(function(btnName, $button, btnObject, e) {
this.dropdownShow(e, btnName);
- }, this))
+ }, this));
+ //this.buttonAwesome('fontcolor', 'fa-font');
},
/**
_onColorPick: function(event) {
event.preventDefault();
- this.bufferSet();
+ //this.bufferSet();
- this.$editor.focus();
- this.inlineRemoveStyle('color');
+ //this.$editor.focus();
- var $type = $(event.currentTarget).data('color');
- if ($type !== 'none') this.inlineSetStyle('color', $type);
- if (this.opts.air) this.$air.fadeOut(100);
- this.sync();
+ var $color = $(event.currentTarget).data('color');
+ if ($color === 'none') {
+ this.inlineRemoveStyle('color');
+ }
+ else {
+ this.inlineSetStyle('color', $color);
+ }
+
+ /*if (this.opts.air) this.$air.fadeOut(100);
+ this.sync();*/
}
};
\ No newline at end of file
--- /dev/null
+if (!RedactorPlugins) var RedactorPlugins = {};
+
+/**
+ * Provides a font family picker, this is actually a heavily modified version of Imperavi's 'fontfamily' plugin.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2014 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+RedactorPlugins.wfontfamily = {
+ init: function () {
+ var $fonts = {
+ 'Arial': "Arial, Helvetica, sans-serif",
+ 'Comic Sans MS': "Comic Sans MS, cursive",
+ 'Courier New': "Consolas, Courier New, Courier, monospace",
+ 'Georgia': "Georgia, serif",
+ 'Lucida Sans Unicode': "Lucida Sans Unicode, Lucida Grande, sans-serif",
+ 'Tahoma': "Tahoma, Geneva, sans-serif",
+ 'Times New Roman': "Times New Roman, Times, serif",
+ 'Trebuchet MS': "Trebuchet MS, Helvetica, sans-serif",
+ 'Verdana': "Verdana, Geneva, sans-serif"
+ };
+
+ var $dropdown = { };
+ var $i = 0;
+ var self = this;
+ $.each($fonts, function(title, value) {
+ $dropdown['fontFamily' + $i] = {
+ title: title,
+ className: 'wfontfamily-' + $i,
+ callback: function() {
+ self.inlineSetStyle('font-family', value);
+ }
+ };
+
+ $i++;
+ });
+ $dropdown['separator'] = { name: 'separator' };
+ $dropdown['remove'] = {
+ title: 'remove font',
+ callback: function() {
+ this.inlineRemoveStyle('font-family');
+ }
+ };
+
+ this.buttonReplace('fontfamily', 'wfontfamily', 'Change font family', false, $dropdown);
+ this.buttonGet('wfontfamily').addClass('re-fontfamily');
+
+ // modify dropdown to reflect each font family
+ $dropdown = this.$toolbar.find('.redactor_dropdown_box_wfontfamily');
+ $i = 0;
+ $.each($fonts, function(title, value) {
+ $dropdown.children('.wfontfamily-' + $i).removeClass('wfontfamily-' + $i).css('font-family', value);
+ $i++;
+ });
+ }
+};
\ No newline at end of file
--- /dev/null
+if (!RedactorPlugins) var RedactorPlugins = {};
+
+/**
+ * Provides a font size picker, this is actually a heavily modified version of Imperavi's 'fontsize' plugin.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2014 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+RedactorPlugins.wfontsize = {
+ init: function () {
+ var $fontSizes = [ 8, 10, 12, 14, 18, 24, 36 ];
+
+ var $dropdown = { };
+ var self = this;
+ for (var $i = 0, $length = $fontSizes.length; $i < $length; $i++) {
+ var $fontSize = $fontSizes[$i];
+
+ $dropdown['fontSize' + $i] = {
+ title: $fontSize,
+ className: 'wfontsize-' + $fontSize,
+ callback: function() {
+ self.inlineSetStyle('font-size', $fontSize + 'pt');
+ }
+ };
+ }
+
+ $dropdown['separator'] = { name: 'separator' };
+ $dropdown['remove'] = {
+ title: 'remove font size',
+ callback: function() {
+ this.inlineRemoveStyle('font-size');
+ }
+ };
+
+ this.buttonReplace('fontsize', 'wfontsize', 'Change font size', false, $dropdown);
+ this.buttonGet('wfontsize').addClass('re-fontsize');
+
+ // modify dropdown to reflect each font family
+ $dropdown = this.$toolbar.find('.redactor_dropdown_box_wfontsize');
+ for (var $i = 0, $length = $fontSizes.length; $i < $length; $i++) {
+ var $fontSize = $fontSizes[$i];
+
+ var $listItem = $dropdown.children('a.wfontsize-' + $fontSize).removeClass('wfontsize-' + $fontSizes).css('font-size', $fontSize + 'pt');
+ if ($fontSize > 18) {
+ $listItem.css('line-height', '1em');
+ }
+ }
+ }
+};
self.$source.height($height);
};
+
+ var $mpModalInit = this.modalInit;
+ this.modalInit = function(title, content, width, callback) {
+ self.mpModalInit();
+
+ $mpModalInit.call(self, title, content, width, callback);
+ };
+
+ this.setOption('modalOpenedCallback', $.proxy(this.modalOpenedCallback, this));
+
+ this.modalTemplatesInit();
},
/**
}
}
},
+
+ /**
+ * Provides WCF-like overlays.
+ */
+ modalTemplatesInit: function() {
+ this.setOption('modal_image',
+ '<fieldset>'
+ + '<dl>'
+ + '<dt><label for="redactor_file_link">' + this.opts.curLang.image_web_link + '</label></dt>'
+ + '<dd><input type="text" name="redactor_file_link" id="redactor_file_link" class="long" /></dd>'
+ + '</dl>'
+ + '</fieldset>'
+ + '<div class="formSubmit">'
+ + '<button id="redactor_upload_btn">' + this.opts.curLang.insert + '</button>'
+ + '</div>'
+ );
+
+ this.setOption('modal_table',
+ '<fieldset>'
+ + '<dl>'
+ + '<dt><label for="redactor_table_rows">' + this.opts.curLang.rows + '</label></dt>'
+ + '<dd><input type="number" size="5" value="2" min="0" id="redactor_table_rows" class="tiny" /></dd>'
+ + '</dl>'
+ + '<dl>'
+ + '<dt><label for="redactor_table_columns">' + this.opts.curLang.columns + '</label></dt>'
+ + '<dd><input type="number" size="5" value="3" min="0" id="redactor_table_columns" class="tiny" /></dd>'
+ + '</dl>'
+ + '</fieldset>'
+ + '<div class="formSubmit">'
+ + '<button id="redactor_insert_table_btn">' + this.opts.curLang.insert + '</button>'
+ + '</div>'
+ );
+
+ $.extend( this.opts, {
+ modal_file: String()
+ + '<section id="redactor-modal-file-insert">'
+ + '<div id="redactor-progress" class="redactor-progress-inline" style="display: none;"><span></span></div>'
+ + '<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>',
+
+ 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>',
+
+ // img
+
+ modal_link: String()
+ + '<section id="redactor-modal-link-insert">'
+ + '<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>',
+
+ 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>'
+
+ });
+ },
+
+ mpModalInit: function() {
+ // modal overlay
+ if (!$('#redactor_modal_overlay').length) {
+ this.$overlay = $('<div id="redactor_modal_overlay" class="dialogOverlay" />').css({ height: '100%', zIndex: 50000 }).hide().appendTo(document.body);
+ }
+
+ if (!$('#redactor_modal').length) {
+ this.$modal = $('<div id="redactor_modal" class="dialogContainer" />').css({ display: 'none', zIndex: 50001 }).appendTo(document.body);
+ $('<header class="dialogTitlebar"><span id="redactor_modal_header" class="dialogTitle" /><a id="redactor_modal_close" class="dialogCloseButton" /></header>').appendTo(this.$modal);
+ $('<div class="dialogContent"><div id="redactor_modal_inner" /></div>').appendTo(this.$modal);
+ }
+
+ this.$modal.children('.dialogContent').removeClass('dialogForm');
+ },
+
+ modalOpenedCallback: function() {
+ // handle positioning of form submit controls
+ var $heightDifference = 0;
+ if (this.$modal.find('.formSubmit').length) {
+ $heightDifference = this.$modal.find('.formSubmit').outerHeight();
+
+ this.$modal.children('.dialogContent').addClass('dialogForm').css({ marginBottom: $heightDifference + 'px' });
+ }
+ else {
+ this.$modal.children('.dialogContent').removeClass('dialogForm').css({ marginBottom: '0px' });
+ }
+
+ // fix position
+ var $dimensions = this.$modal.getDimensions('outer');
+ this.$modal.css({
+ marginLeft: -1 * Math.round($dimensions.width / 2) + 'px',
+ marginTop: -1 * Math.round($dimensions.height / 2) + 'px'
+ });
+ }
}
\ No newline at end of file
background: none !important;
box-shadow: none !important;
}
+
+.redactor_editor p {
+ margin-bottom: 0 !important;
+}
+
.redactor_editor iframe,
.redactor_editor object,
.redactor_editor hr {
/*
MODAL
*/
-#redactor_modal_overlay {
+/*#redactor_modal_overlay {
position: fixed;
top: 0;
left: 0;
}
#redactor_modal_close:hover {
color: #000;
-}
+}*/
.redactor_input {
width: 99%;
font-size: 14px;
if ((range.collapsed || range.startContainer === range.endContainer) && el && !this.nodeTestBlocks(el))
{
+ console.debug("1");
$(el)[type](attr, value);
}
else
{
+ console.debug("2");
this.document.execCommand('fontSize', false, 4 );
var fonts = this.$editor.find('font');
}
> li {
- &:last-child,
+ &:last-of-type,
&.separator {
border-right: 1px solid @wcfButtonBorderColor;
}
}
}
- &.re-icon:before {
+ &.fa-redactor-btn:before {
content: "";
}
}