From: Alexander Ebert Date: Fri, 3 Jul 2015 10:56:51 +0000 (+0200) Subject: Added support for images (embedded attachments missing for now) X-Git-Tag: 3.0.0_Beta_1~2225 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=f6fe73778ca9ffe3bd3c1383f909edb1f7bf2b74;p=GitHub%2FWoltLab%2FWCF.git Added support for images (embedded attachments missing for now) --- diff --git a/com.woltlab.wcf/templates/wysiwyg.tpl b/com.woltlab.wcf/templates/wysiwyg.tpl index c8f1477bae..b270eaac26 100644 --- a/com.woltlab.wcf/templates/wysiwyg.tpl +++ b/com.woltlab.wcf/templates/wysiwyg.tpl @@ -2,6 +2,7 @@ var __REDACTOR_ICON_PATH = '{@$__wcf->getPath()}icon/'; var __REDACTOR_BUTTONS = [ {implode from=$__wcf->getBBCodeHandler()->getButtonBBCodes() item=__bbcode}{ icon: '{$__bbcode->wysiwygIcon}', label: '{$__bbcode->buttonLabel|language}', name: '{$__bbcode->bbcodeTag}' }{/implode} ]; var __REDACTOR_SMILIES = { {implode from=$__wcf->getSmileyCache()->getCategorySmilies() item=smiley}'{@$smiley->smileyCode|encodeJS}': '{@$smiley->getURL()|encodeJS}'{/implode} }; +var __REDACTOR_BBCODES = [ {implode from=$__wcf->getBBCodeHandler()->getBBCodes() item=__bbcode}'{@$__bbcode->bbcodeTag}'{/implode} ]; var __REDACTOR_SOURCE_BBCODES = [ {implode from=$__wcf->getBBCodeHandler()->getSourceBBCodes() item=__bbcode}'{@$__bbcode->bbcodeTag}'{/implode} ]; var __REDACTOR_CODE_HIGHLIGHTERS = { {implode from=$__wcf->getBBCodeHandler()->getHighlighters() item=__highlighter}'{@$__highlighter}': '{lang}wcf.bbcode.code.{@$__highlighter}.title{/lang}'{/implode} }; var __REDACTOR_AMD_DEPENDENCIES = { }; diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js index ea74dff772..19640e5f82 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js @@ -302,12 +302,6 @@ RedactorPlugins.wbbcode = function() { WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'beforeConvertFromHtml', { html: html }); - // remove data-redactor-tag="" attribute - html = html.replace(/(<[^>]+?) data-redactor-tag="[^"]+"/g, '$1'); - - // remove rel="" attribute - html = html.replace(/(<[^>]+?) rel="[^"]+"/g, '$1'); - // remove zero-width space sometimes slipping through html = html.replace(/&#(8203|x200b);/g, ''); @@ -318,78 +312,6 @@ RedactorPlugins.wbbcode = function() { html = html.replace(/—/gi, '\u2014'); html = html.replace(/‐/gi, '\u2010'); - // preserve code listings - var $cachedCodeListings = { }; - html = html.replace(/]+?)class="codeBox[^"]+"([^>]*?)>\n*
[\s\S]+?
    ([\s\S]+?)<\/ol>\n*<\/div>\n*<\/div>/g, function(match, codeBoxAttributes1, codeBoxAttributes2, lineNumber, codeContent) { - var $attributes = codeBoxAttributes1 + ' ' + codeBoxAttributes2; - var $highlighter = ''; - var $filename = ''; - if ($attributes.match(/data-highlighter="([a-zA-Z]+)"/)) { - $highlighter = RegExp.$1; - } - if ($attributes.match(/data-filename="([^"]+)"/)) { - $filename = $.trim(RegExp.$1); - } - - var $uuid = WCF.getUUID(); - $cachedCodeListings[$uuid] = { - codeContent: codeContent.replace(/
  1. /g, '').replace(/<\/li>/g, '\n').replace(/\n$/, ''), - filename: $filename.replace(/['"]/g, ''), - highlighter: ($highlighter === 'plain' ? '' : $highlighter), - lineNumber: (lineNumber > 1 ? lineNumber : 0) - }; - - return '@@@' + $uuid + '@@@'; - }); - - // unwrap code boxes - for (var $uuid in $cachedCodeListings) { - html = html.replace(new RegExp('

    <\/p>@@@' + $uuid + '@@@

    <\/p>'), '@@@' + $uuid + '@@@'); - } - - // handle empty paragraphs not followed by an empty one - html = html.replace(/

    <\/p>

    (?!
    )/g, '

    @@@wcf_empty_line@@@

    '); - - html = html.replace(/@@@wcf_empty_line@@@/g, '\n'); - html = html.replace(/\n\n$/, '\n'); - - - // drop
    , they are pointless because the editor already adds a newline after them - html = html.replace(/
    /g, ''); - html = html.replace(/ /gi, " "); - - // [quote] - html = html.replace(/]+)>\n?]*?>[\s\S]*?<\/header>/gi, function(match, attributes, innerContent) { - var $quote; - var $author = ''; - var $link = ''; - - if (attributes.match(/data-author="([^"]+)"/)) { - $author = WCF.String.unescapeHTML(RegExp.$1); - } - - if (attributes.match(/cite="([^"]+)"/)) { - $link = WCF.String.unescapeHTML(RegExp.$1); - } - - if ($link) { - $quote = "[quote='" + $author + "','" + $link + "']"; - } - else if ($author) { - $quote = "[quote='" + $author + "']"; - } - else { - $quote = "[quote]"; - } - - return $quote; - }); - html = html.replace(/(?:\n*)<\/blockquote>\n?/gi, '\n[/quote]\n'); - - // smileys - html = html.replace(/ ?]*?alt="([^"]+?)"[^>]*?class="smiley"[^>]*?> ?/gi, ' $1 '); // firefox - html = html.replace(/ ?]*?class="smiley"[^>]*?alt="([^"]+?)"[^>]*?> ?/gi, ' $1 '); // chrome, ie - // attachments html = html.replace(/]*?)class="[^"]*redactorEmbeddedAttachment[^"]*"([^>]*?)>/gi, function(match, attributesBefore, attributesAfter) { var $attributes = attributesBefore + ' ' + attributesAfter; @@ -428,41 +350,6 @@ RedactorPlugins.wbbcode = function() { return '[attach=' + $attachmentID + '][/attach]'; }); - // [img] - html = html.replace(/]*)?src=(["'])([^"']+?)\2([^>]*)?>/gi, function(match, attributesBefore, quotationMarks, source, attributesAfter) { - var attrs = attributesBefore + " " + attributesAfter; - var style = ''; - if (attrs.match(/style="([^"]+)"/)) { - style = RegExp.$1; - } - - var $float = 'none'; - var $width = 0; - - var $styles = style.split(';'); - for (var $i = 0; $i < $styles.length; $i++) { - var $style = $styles[$i]; - if ($style.match(/float: (left|right|none)/)) { - $float = RegExp.$1; - } - else if ($style.match(/width: (\d+)px/)) { - $width = parseInt(RegExp.$1); - } - } - - if ($width) { - return "[img='" + source + "'," + $float + "," + $width + "][/img]"; - } - else if ($float !== 'none') { - return "[img='" + source + "'," + $float + "][/img]"; - } - - return "[img]" + source + "[/img]"; - }); - - // [td]+[align] - html = html.replace(/([\s\S]*?)<\/td>/gi, "[td][align=$1]$2[/align][/td]"); - // cache redactor's selection markers var $cachedMarkers = { }; html.replace(/<\/span>/, function(match) { @@ -484,56 +371,6 @@ RedactorPlugins.wbbcode = function() { } } - // restore code listings - if ($.getLength($cachedCodeListings)) { - $.each($cachedCodeListings, function(uuid, listing) { - var $count = 0; - if (listing.highlighter) $count++; - if (listing.lineNumber) $count++; - if (listing.filename) $count++; - - var $attributes = ''; - switch ($count) { - case 1: - if (listing.highlighter) { - $attributes = listing.highlighter; - } - else if (listing.filename) { - $attributes = "'" + listing.filename + "'"; - } - else { - $attributes = listing.lineNumber; - } - break; - - case 2: - if (listing.lineNumber) { - $attributes = listing.lineNumber; - } - - if (listing.highlighter) { - if ($attributes.length) $attributes += ','; - $attributes += listing.highlighter; - } - - if (listing.filename) { - if ($attributes.length) $attributes += ','; - - $attributes += "'" + listing.filename + "'"; - } - break; - - case 3: - $attributes = listing.highlighter + ',' + listing.lineNumber + ",'" + listing.filename + "'"; - break; - } - - var $bbcode = '[code' + ($attributes.length ? '=' + $attributes : '') + ']' + listing.codeContent + '[/code]\n'; - - html = html.replace(new RegExp('@@@' + uuid + '@@@\n?', 'g'), $bbcode); - }); - } - // Restore <, > and & html = html.replace(/</g, '<'); html = html.replace(/>/g, '>'); @@ -545,12 +382,6 @@ RedactorPlugins.wbbcode = function() { WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'afterConvertFromHtml', { html: html }); - // remove all leading and trailing whitespaces, but add one empty line at the end - html = $.trim(html); - if (html.length) { - html += "\n"; - } - return html; }, @@ -568,15 +399,6 @@ RedactorPlugins.wbbcode = function() { // remove 0x200B (unicode zero width space) data = this.wutil.removeZeroWidthSpace(data); - // cache source code tags - var $cachedCodes = [ ]; - var $regExp = new RegExp('\\[(' + __REDACTOR_SOURCE_BBCODES.join('|') + ')([\\S\\s]+?)\\[\\/\\1\\]', 'gi'); - data = data.replace($regExp, function(match) { - var $key = match.hashCode(); - $cachedCodes.push({ key: $key, value: match.replace(/\$/g, '$$$$') }); - return '@@' + $key + '@@'; - }); - // [email] data = data.replace(/\[email\]([^"]+?)\[\/email]/gi, '$1' + this.opts.invisibleSpace); data = data.replace(/\[email\=([^"\]]+)](.+?)\[\/email]/gi, '$2' + this.opts.invisibleSpace); @@ -682,267 +504,6 @@ RedactorPlugins.wbbcode = function() { // remove "javascript:" data = data.replace(/(javascript):/gi, '$1:'); - // extract [quote] bbcodes to prevent line break handling below - var $cachedQuotes = [ ]; - var $knownQuotes = [ ]; - - var $parts = data.split(/(\[(?:\/quote|quote|quote='[^']*?'(?:,'[^']*?')?|quote="[^"]*?"(?:,"[^"]*?")?)\])/i); - var $lostQuote = WCF.getUUID(); - while (true) { - var $foundClosingTag = false; - for (var $i = 0; $i < $parts.length; $i++) { - var $part = $parts[$i]; - if ($part.toLowerCase() === '[/quote]') { - $foundClosingTag = true; - - var $content = ''; - var $previous = $parts.slice(0, $i); - var $foundOpenTag = false; - while ($previous.length) { - var $prev = $previous.pop(); - $content = $prev + $content; - if ($prev.match(/^\[quote/i)) { - $part = $content + $part; - - var $key = WCF.getUUID(); - $cachedQuotes.push({ - hashCode: $key, - content: $part.replace(/\$/g, '$$$$') - }); - $knownQuotes.push($key); - - $part = '@@' + $key + '@@'; - $foundOpenTag = true; - break; - } - } - - if (!$foundOpenTag) { - $previous = $parts.slice(0, $i); - $part = $lostQuote; - } - - // rebuild the array - $parts = $previous.concat($part, $parts.slice($i + 1)); - - break; - } - } - - if (!$foundClosingTag) { - break; - } - } - - data = $parts.join(''); - - // restore unmatched closing quote tags - data = data.replace(new RegExp($lostQuote, 'g'), '[/quote]'); - - // drop trailing line breaks - data = data.replace(/\n*$/, ''); - - // insert quotes - if ($cachedQuotes.length) { - // [quote] - var $unquoteString = function(quotedString) { - return quotedString.replace(/^['"]/, '').replace(/['"]$/, ''); - }; - - var self = this; - var $transformQuote = function(quote) { - return quote.replace(/\[quote(=(['"]).+?\2)?\]([\S\s]*)\[\/quote\]/gi, function(match, attributes, quotationMark, innerContent) { - var $author = ''; - var $link = ''; - - if (attributes) { - attributes = attributes.substr(1); - attributes = attributes.split(','); - - switch (attributes.length) { - case 1: - $author = attributes[0]; - break; - - case 2: - $author = attributes[0]; - $link = attributes[1]; - break; - } - - $author = WCF.String.escapeHTML($unquoteString($.trim($author))); - $link = WCF.String.escapeHTML($unquoteString($.trim($link))); - } - - var $quote = '

    ' - + '
    ' - + '

    ' - + self.wbbcode._buildQuoteHeader($author, $link) - + '

    ' - + '' - + '
    '; - - innerContent = $.trim(innerContent); - var $tmp = ''; - - if (innerContent.length) { - var $lines = innerContent.split('\n'); - for (var $i = 0; $i < $lines.length; $i++) { - var $line = $lines[$i]; - if ($line.length === 0) { - $line = self.opts.invisibleSpace; - } - else if ($line.match(/^@@([0-9\-]+)@@$/)) { - if (WCF.inArray(RegExp.$1, $knownQuotes)) { - // prevent quote being nested inside a
    block - $tmp += $line; - continue; - } - } - - $tmp += '
    ' + $line + '
    '; - } - } - else { - $tmp = '
    ' + self.opts.invisibleSpace + '
    '; - } - - $quote += $tmp; - $quote += '
    '; - - return $quote; - }); - }; - - // reinsert quotes in reverse order, adding the most outer quotes first - for (var $i = $cachedQuotes.length - 1; $i >= 0; $i--) { - var $cachedQuote = $cachedQuotes[$i]; - var $regex = new RegExp('@@' + $cachedQuote.hashCode + '@@', 'g'); - data = data.replace($regex, $transformQuote($cachedQuote.content)); - } - } - - // insert codes - if ($cachedCodes.length) { - for (var $i = $cachedCodes.length - 1; $i >= 0; $i--) { - var $cachedCode = $cachedCodes[$i]; - var $regex = new RegExp('@@' + $cachedCode.key + '@@', 'g'); - var $value = $cachedCode.value; - - // [tt] - $value = $value.replace(/^\[tt\]([\s\S]+)\[\/tt\]/, (function(match, content) { - var $tmp = content.split("\n"); - content = ''; - - for (var $i = 0, $length = $tmp.length; $i < $length; $i++) { - var $line = $tmp[$i]; - - if ($line.length) { - if (content.length) content += '

    '; - - content += '[tt]' + $line + '[/tt]'; - } - else { - if ($i === 0 || ($i + 1) === $length) { - // ignore the first and last empty element - continue; - } - - if (content.match(/\[\/tt\]$/)) { - content += '

    ' + this.opts.invisibleSpace + ''; - } - else { - content += '


    '; - } - } - } - - return content; - }).bind(this)); - - // [code] - $value = $value.replace(/^\[code([^\]]*)\]([\S\s]*)\[\/code\]$/, (function(matches, parameters, content) { - var $highlighter = 'plain'; - var $lineNumber = 0; - var $filename = ''; - - if (parameters) { - parameters = parameters.substring(1); - parameters = parameters.split(','); - - var $isNumber = function(string) { return string.match(/^\d+$/); }; - var $isFilename = function(string) { return (string.indexOf('.') !== -1) || (string.match(/^(["']).*\1$/)); }; - var $isHighlighter = function(string) { return (__REDACTOR_CODE_HIGHLIGHTERS[string] !== undefined); }; - - var $unquoteFilename = function(filename) { - return filename.replace(/^(["'])(.*)\1$/, '$2'); - }; - - switch (parameters.length) { - case 1: - if ($isNumber(parameters[0])) { - $lineNumber = (parseInt(parameters[0]) > 1) ? parameters[0] : 0; - } - else if ($isFilename(parameters[0])) { - $filename = $unquoteFilename(parameters[0]); - } - else if ($isHighlighter(parameters[0])) { - $highlighter = parameters[0]; - } - break; - - case 2: - if ($isNumber(parameters[0])) { - $lineNumber = (parseInt(parameters[0]) > 1) ? parameters[0] : 0; - - if ($isHighlighter(parameters[1])) { - $highlighter = parameters[1]; - } - else if ($isFilename(parameters[1])) { - $filename = $unquoteFilename(parameters[1]); - } - } - else { - if ($isHighlighter(parameters[0])) $highlighter = parameters[0]; - if ($isFilename(parameters[1])) $filename = $unquoteFilename(parameters[1]); - } - break; - - case 3: - if ($isHighlighter(parameters[0])) $highlighter = parameters[0]; - if ($isNumber(parameters[1])) $lineNumber = parameters[1]; - if ($isFilename(parameters[2])) $filename = $unquoteFilename(parameters[2]); - break; - } - } - - content = content.replace(/^\n+/, '').replace(/\n+$/, '').split(/\n/); - var $lines = ''; - for (var $i = 0; $i < content.length; $i++) { - var $line = content[$i]; - if (!$line.length) { - $line = this.opts.invisibleSpace; - } - - $lines += '

  2. ' + $line + '
  3. '; - } - - return '
    ' - + '
    ' - + '
    ' - + '

    ' + __REDACTOR_CODE_HIGHLIGHTERS[$highlighter] + ($filename ? ': ' + WCF.String.escapeHTML($filename) : '') + '

    ' - + '
    ' - + '
      ' - + $lines - + '
    ' - + '
    ' - + '
    '; - }).bind(this)); - - data = data.replace($regex, $value); - } - } - WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'afterConvertToHtml', { data: data }); return data; diff --git a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js index 1aa3626fd6..e196979d33 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js +++ b/wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js @@ -667,7 +667,7 @@ RedactorPlugins.wmonkeypatch = function() { anchor.attr('href', link); } } - else { + else if (link !== '') { anchor = document.createElement('a'); anchor.href = link; image[0].parentNode.insertBefore(anchor, image[0]); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js index 0e3cafdffa..9382df0521 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js @@ -95,6 +95,7 @@ define(['EventHandler', 'StringUtil', 'DOM/Traverse'], function(EventHandler, St // callback replacement { tagName: 'A', callback: this._convertUrl.bind(this) }, + { tagName: 'IMG', callback: this._convertImage.bind(this) }, { tagName: 'LI', callback: this._convertListItem.bind(this) }, { tagName: 'OL', callback: this._convertList.bind(this) }, { tagName: 'TABLE', callback: this._convertTable.bind(this) }, @@ -173,6 +174,32 @@ define(['EventHandler', 'StringUtil', 'DOM/Traverse'], function(EventHandler, St element.outerHTML = open + element.innerHTML.replace(/^\n*/, '').replace(/\n*$/, '') + '[/quote]\n'; }, + _convertImage: function(element) { + if (element.classList.contains('smiley')) { + // smiley + element.outerHTML = ' ' + element.getAttribute('alt') + ' '; + } + else if (element.classList.contains('redactorEmbeddedAttachment')) { + // TODO: handle attachments + } + else { + // regular image + var float = element.style.getPropertyValue('float') || 'none'; + var source = element.src.trim(); + var width = ~~element.style.getPropertyValue('width').replace(/px$/, '') || 0; + + if (width > 0) { + element.outerHTML = "[img='" + source + "'," + float + "," + width + "][/img]"; + } + else if (float !== 'none') { + element.outerHTML = "[img='" + source + "'," + float + "][/img]"; + } + else { + element.outerHTML = "[img]" + source + "[/img]"; + } + } + }, + _convertList: function(element) { var open; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js index 9e72dc9d54..2ea34d0f4e 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js @@ -11,7 +11,7 @@ define([], function() { _splitTags: function(message) { // TODO: `validTags` should be dynamic similar to the PHP implementation - var validTags = 'attach|b|code|color|i|list|url|table|td|tr|quote'; + var validTags = __REDACTOR_BBCODES.join('|'); var pattern = '(\\\[(?:/(?:' + validTags + ')|(?:' + validTags + ')' + '(?:=' + '(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\\\'|[^,\\\]]*)' diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js index 9a40c18d1f..8fb7e5d2de 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js @@ -65,7 +65,8 @@ define(['EventHandler', 'Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], code: this._replaceCode.bind(this), list: this._replaceList.bind(this), quote: this._replaceQuote.bind(this), - url: this._replaceUrl.bind(this) + url: this._replaceUrl.bind(this), + img: this._replaceImage.bind(this) }; _removeNewlineAfter = ['quote', 'table', 'td', 'tr']; @@ -220,6 +221,54 @@ define(['EventHandler', 'Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], return ''; }, + _replaceImage: function(stack, item, index) { + stack[item.pair] = ''; + + var float = 'none', source = '', width = 0; + + switch (item.attributes.length) { + case 0: + if (index + 1 < item.pair && typeof stack[index + 1] === 'string') { + source = stack[index + 1]; + stack[index + 1] = ''; + } + else { + // [img] without attributes and content, discard + return ''; + } + break; + + case 1: + source = item.attributes[0]; + break; + + case 2: + source = item.attributes[0]; + float = item.attributes[1]; + break; + + case 3: + source = item.attributes[0]; + float = item.attributes[1]; + width = ~~item.attributes[2]; + break; + } + + if (float !== 'left' && float !== 'right') float = 'none'; + + var styles = []; + if (width > 0) { + styles.push('width: ' + width + 'px'); + } + + if (float !== 'none') { + styles.push('float: ' + float); + styles.push('margin: ' + (float === 'left' ? '0 15px 7px 0' : '0 0 7px 15px')); + } + + return ''; + }, + _replaceList: function(stack, item, index) { var type = (items.attributes.length) ? item.attributes[0] : ''; diff --git a/wcfsetup/install/files/lib/system/bbcode/BBCodeHandler.class.php b/wcfsetup/install/files/lib/system/bbcode/BBCodeHandler.class.php index be41975ce4..0a64c0ebf2 100644 --- a/wcfsetup/install/files/lib/system/bbcode/BBCodeHandler.class.php +++ b/wcfsetup/install/files/lib/system/bbcode/BBCodeHandler.class.php @@ -65,6 +65,15 @@ class BBCodeHandler extends SingletonFactory { return in_array($bbCodeTag, $this->allowedBBCodes); } + /** + * Returns all bbcodes. + * + * @return array<\wcf\data\bbcode\BBCode> + */ + public function getBBCodes() { + return BBCodeCache::getInstance()->getBBCodes(); + } + /** * Returns a list of BBCodes displayed as buttons. * diff --git a/wcfsetup/install/files/style/redactor.less b/wcfsetup/install/files/style/redactor.less index e92182fb49..a49a7065f8 100644 --- a/wcfsetup/install/files/style/redactor.less +++ b/wcfsetup/install/files/style/redactor.less @@ -326,7 +326,9 @@ left: 50%; line-height: @wcfSmallFontSize; margin-top: -13px; + opacity: 1; top: 50%; + visibility: visible; z-index: 5; }