Added support for spoilers in WYSIWYG
authorAlexander Ebert <ebert@woltlab.com>
Thu, 16 Jun 2016 09:40:49 +0000 (11:40 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 16 Jun 2016 09:40:54 +0000 (11:40 +0200)
com.woltlab.wcf/templates/wysiwyg.tpl
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabBlock.js [new file with mode: 0644]
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabSpoiler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Spoiler.js [new file with mode: 0644]
wcfsetup/install/files/style/bbcode/spoiler.scss
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 065a7f06c3dedae7dfeba683b40da63857a60c96..26768abafc7ed7b9aec0b7afb37737934168c8d2 100644 (file)
@@ -19,6 +19,7 @@
                        
                        {* WoltLab *}
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabAttachment.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabBlock.js?v={@LAST_UPDATE_TIME}',
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabButton.js?v={@LAST_UPDATE_TIME}',
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabCode.js?v={@LAST_UPDATE_TIME}',
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabColor.js?v={@LAST_UPDATE_TIME}',
@@ -32,7 +33,8 @@
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabModal.js?v={@LAST_UPDATE_TIME}',
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabQuote.js?v={@LAST_UPDATE_TIME}',
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabSize.js?v={@LAST_UPDATE_TIME}',
-                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabSmiley.js?v={@LAST_UPDATE_TIME}'
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabSmiley.js?v={@LAST_UPDATE_TIME}',
+                       '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabSpoiler.js?v={@LAST_UPDATE_TIME}'
                {else}
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/redactor.min.js?v={@LAST_UPDATE_TIME}',
                        '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/combined.min.js?v={@LAST_UPDATE_TIME}'
                                'wcf.editor.quote.edit': '{lang}wcf.editor.quote.edit{/lang}',
                                'wcf.editor.quote.title': '{lang __literal=true}wcf.editor.quote.title{/lang}',
                                'wcf.editor.quote.url': '{lang}wcf.editor.quote.url{/lang}',
-                               'wcf.editor.quote.url.description': '{lang}wcf.editor.quote.url.description{/lang}'
+                               'wcf.editor.quote.url.description': '{lang}wcf.editor.quote.url.description{/lang}',
+                               
+                               'wcf.editor.spoiler.label': '{lang}wcf.editor.spoiler.label{/lang}',
+                               'wcf.editor.spoiler.label.description': '{lang}wcf.editor.spoiler.label.description{/lang}',
+                               'wcf.editor.spoiler.edit': '{lang}wcf.editor.spoiler.edit{/lang}',
+                               'wcf.editor.spoiler.title': '{lang __literal=true}wcf.editor.spoiler.title{/lang}'
                        });
                        
                        var buttons = [], buttonOptions = [], customButtons = [];
@@ -92,6 +99,7 @@
                                        'source',
                                        'table',
                                        'WoltLabAttachment',
+                                       'WoltLabBlock',
                                        'WoltLabCode',
                                        'WoltLabColor',
                                        'WoltLabDropdown',
                                        'WoltLabModal',
                                        'WoltLabQuote',
                                        'WoltLabSize',
-                                       'WoltLabSmiley'
+                                       'WoltLabSmiley',
+                                       'WoltLabSpoiler'
                                ],
                                toolbarFixed: false,
                                woltlab: {
diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabBlock.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabBlock.js
new file mode 100644 (file)
index 0000000..f64d35a
--- /dev/null
@@ -0,0 +1,47 @@
+$.Redactor.prototype.WoltLabBlock = function() {
+       "use strict";
+       
+       return {
+               init: function() {
+                       this.block.tags = ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'figure'];
+                       
+                       this.block.format = (function(tag, attr, value, type) {
+                               tag = (tag === 'quote') ? 'blockquote' : tag;
+                               
+                               // WoltLab modification: move list of allowed elements
+                               // outside this method to allow extending it
+                               //
+                               //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);
+                       }).bind(this);
+               },
+               
+               register: function(tag) {
+                       if (this.block.tags.indexOf(tag) !== -1) {
+                               return;
+                       }
+                       
+                       this.block.tags.push(tag);
+                       
+                       if (this.opts.blockTags.indexOf(tag) === -1) {
+                               this.opts.blockTags.push(tag);
+                               
+                               this.reIsBlock = new RegExp('^(' + this.opts.blockTags.join('|').toUpperCase() + ')$', 'i');
+                       }
+               }
+       }
+};
diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabSpoiler.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabSpoiler.js
new file mode 100644 (file)
index 0000000..a929596
--- /dev/null
@@ -0,0 +1,11 @@
+$.Redactor.prototype.WoltLabSpoiler = function() {
+       "use strict";
+       
+       return {
+               init: function() {
+                       require(['WoltLab/WCF/Ui/Redactor/Spoiler'], (function (UiRedactorSpoiler) {
+                               new UiRedactorSpoiler(this);
+                       }).bind(this));
+               }
+       };
+};
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Spoiler.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Spoiler.js
new file mode 100644 (file)
index 0000000..08c2648
--- /dev/null
@@ -0,0 +1,183 @@
+/**
+ * Manages spoilers.
+ *
+ * @author      Alexander Ebert
+ * @copyright   2001-2016 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module      WoltLab/WCF/Ui/Redactor/Spoiler
+ */
+define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
+       "use strict";
+       
+       var _headerHeight = 0;
+       
+       /**
+        * @param       {Object}        editor  editor instance
+        * @constructor
+        */
+       function UiRedactorSpoiler(editor) { this.init(editor); }
+       UiRedactorSpoiler.prototype = {
+               /**
+                * Initializes the spoiler management.
+                * 
+                * @param       {Object}        editor  editor instance
+                */
+               init: function(editor) {
+                       this._editor = editor;
+                       this._elementId = this._editor.$element[0].id;
+                       this._spoiler = null;
+                       
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_spoiler_' + this._elementId, this._bbcodeSpoiler.bind(this));
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
+                       
+                       // register custom block element
+                       this._editor.WoltLabBlock.register('woltlab-spoiler');
+                       this._editor.block.tags.push('woltlab-spoiler');
+                       
+                       // support for active button marking
+                       this._editor.opts.activeButtonsStates['woltlab-spoiler'] = 'woltlabSpoiler';
+                       
+                       // static bind to ensure that removing works
+                       this._callbackEdit = this._edit.bind(this);
+                       
+                       // bind listeners on init
+                       this._observeLoad();
+               },
+               
+               /**
+                * Intercepts the insertion of `[spoiler]` tags and uses
+                * the custom `<woltlab-spoiler>` element instead.
+                * 
+                * @param       {Object}        data    event data
+                * @protected
+                */
+               _bbcodeSpoiler: function(data) {
+                       data.cancel = true;
+                       
+                       this._editor.button.toggle({}, 'woltlab-spoiler', 'func', 'block.format');
+                       
+                       var spoiler = this._editor.selection.block();
+                       if (spoiler && spoiler.nodeName === 'WOLTLAB-SPOILER') {
+                               this._setTitle(spoiler);
+                               
+                               spoiler.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                       }
+               },
+               
+               /**
+                * Binds event listeners and sets quote title on both editor
+                * initialization and when switching back from code view.
+                * 
+                * @protected
+                */
+               _observeLoad: function() {
+                       this._editor.events.stopDetectChanges();
+                       
+                       elBySelAll('woltlab-spoiler', this._editor.$editor[0], (function(spoiler) {
+                               spoiler.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                               this._setTitle(spoiler);
+                       }).bind(this));
+                       
+                       this._editor.events.startDetectChanges();
+               },
+               
+               /**
+                * Opens the dialog overlay to edit the spoiler's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _edit: function(event) {
+                       var spoiler = event.currentTarget;
+                       
+                       if (_headerHeight === 0) {
+                               _headerHeight = ~~window.getComputedStyle(spoiler).paddingTop.replace(/px$/, '');
+                               
+                               var styles = window.getComputedStyle(spoiler, '::before');
+                               _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
+                               _headerHeight += ~~styles.height.replace(/px$/, '');
+                               _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
+                       }
+                       
+                       // check if the click hit the header
+                       var offset = DomUtil.offset(spoiler);
+                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
+                               event.preventDefault();
+                               
+                               this._spoiler = spoiler;
+                               
+                               UiDialog.open(this);
+                       }
+               },
+               
+               /**
+                * Saves the changes to the spoiler's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _save: function(event) {
+                       event.preventDefault();
+                       
+                       this._editor.events.stopDetectChanges();
+                       
+                       elData(this._spoiler, 'label', elById('redactor-spoiler-' + this._elementId + '-label').value);
+                       
+                       this._setTitle(this._spoiler);
+                       this._editor.caret.after(this._spoiler);
+                       
+                       this._editor.events.startDetectChanges();
+                       
+                       UiDialog.close(this);
+               },
+               
+               /**
+                * Sets or updates the spoiler's header title.
+                * 
+                * @param       {Element}       spoiler     spoiler element
+                * @protected
+                */
+               _setTitle: function(spoiler) {
+                       var title = Language.get('wcf.editor.spoiler.title', { label: elData(spoiler, 'label') });
+                       
+                       if (elData(spoiler, 'title') !== title) {
+                               elData(spoiler, 'title', title);
+                       }
+               },
+               
+               _dialogSetup: function() {
+                       var id = 'redactor-spoiler-' + this._elementId,
+                           idButtonSave = id + '-button-save',
+                           idLabel = id + '-label';
+                       
+                       return {
+                               id: id,
+                               options: {
+                                       onSetup: (function() {
+                                               elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
+                                       }).bind(this),
+                                       
+                                       onShow: (function() {
+                                               elById(idLabel).value = elData(this._spoiler, 'label');
+                                       }).bind(this),
+                                       
+                                       title: Language.get('wcf.editor.spoiler.edit')
+                               },
+                               source: '<div class="section">'
+                                       + '<dl>'
+                                               + '<dt><label for="' + idLabel + '">' + Language.get('wcf.editor.spoiler.label') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<input type="text" id="' + idLabel + '" class="long">'
+                                                       + '<small>' + Language.get('wcf.editor.spoiler.label.description') + '</small>'
+                                               + '</dd>'
+                                       + '</dl>'
+                               + '</div>'
+                               + '<div class="formSubmit">'
+                                       + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
+                               + '</div>'
+                       };
+               }
+       };
+       
+       return UiRedactorSpoiler;
+});
\ No newline at end of file
index dcf26b8221e69d798bcdd4e3e8e25440f98af025..6da045d1432f686e4605a264cf04b942fe289c27 100644 (file)
@@ -1,3 +1,24 @@
+.redactor-editor woltlab-spoiler {
+       background-color: rgb(255, 255, 255) !important;
+       border: 1px solid rgb(196, 196, 196) !important;
+       border-radius: 2px;
+       display: block;
+       margin: 1em 0;
+       padding: 10px 20px;
+       position: relative;
+       white-space: pre;
+       
+       &::before {
+               content: attr(data-title);
+               cursor: pointer;
+               display: block;
+               margin-bottom: 20px;
+               
+               @include wcfFontHeadline;
+       }
+}
+
+
 .spoilerBox > div {
        border-left: 5px solid $wcfContentBorderInner;
        margin-top: 10px;
index ecb42b8c2db65987f704cea4a672a32c09f36648..06137a288675a31689b271be1028d743bec3276e 100644 (file)
@@ -2157,6 +2157,11 @@ Fehler sind beispielsweise:
                <item name="wcf.editor.quote.title"><![CDATA[{if $author}Zitat von {$author}{else}Zitat{/if}]]></item>
                <item name="wcf.editor.quote.url"><![CDATA[Link]]></item>
                <item name="wcf.editor.quote.url.description"><![CDATA[Optional: Geben Sie einen Link zu der Quelle an.]]></item>
+               
+               <item name="wcf.editor.spoiler.label"><![CDATA[Beschriftung]]></item>
+               <item name="wcf.editor.spoiler.label.description"><![CDATA[Optional: Geben Sie eine Beschriftung für den Spoiler-Button ein.]]></item>
+               <item name="wcf.editor.spoiler.edit"><![CDATA[Spoiler bearbeiten]]></item>
+               <item name="wcf.editor.spoiler.title"><![CDATA[Spoiler{if $label}: {$label}{/if}]]></item>
        </category>
        
        <category name="wcf.global">
index 9f7dfecdb10a4950a838d1560cf06e120a3404af..f6c0fba2c25674e581c15d3b25d22f63f072bfcd 100644 (file)
@@ -2168,6 +2168,11 @@ Errors are:
                <item name="wcf.editor.quote.title"><![CDATA[{if $author}Quote from {$author}{else}Quote{/if}]]></item>
                <item name="wcf.editor.quote.url"><![CDATA[Link]]></item>
                <item name="wcf.editor.quote.url.description"><![CDATA[Optional: Specify the link to the source.]]></item>
+               
+               <item name="wcf.editor.spoiler.label"><![CDATA[Label]]></item>
+               <item name="wcf.editor.spoiler.label.description"><![CDATA[Optional: Specify the label for the spoiler toggle button.]]></item>
+               <item name="wcf.editor.spoiler.edit"><![CDATA[Edit Spoiler]]></item>
+               <item name="wcf.editor.spoiler.title"><![CDATA[Spoiler{if $label}: {$label}{/if}]]></item>
        </category>
        
        <category name="wcf.global">