Added quote support, still getting used to Redactor II
authorAlexander Ebert <ebert@woltlab.com>
Mon, 2 Nov 2015 14:09:18 +0000 (15:09 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 2 Nov 2015 14:09:18 +0000 (15:09 +0100)
com.woltlab.wcf/templates/wysiwyg.tpl
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabDropdown.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabEvent.js [new file with mode: 0644]
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabQuote.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js [new file with mode: 0644]
wcfsetup/install/files/style/bbcode/quote.less [deleted file]
wcfsetup/install/files/style/bbcode/quote.scss [new file with mode: 0644]
wcfsetup/install/files/style/ui/redactor.scss

index da1773e6f7cf4152fc4675b6e98a7548d461f599..475c2235563617e00063d05dafb79d738a2d48db 100644 (file)
@@ -14,7 +14,7 @@
                
                var config = {
                        buttons: buttons,
-                       plugins: ['WoltLabButton', 'WoltLabDropdown'],
+                       plugins: ['WoltLabButton', 'WoltLabDropdown', 'WoltLabEvent', 'WoltLabQuote'],
                        woltlab: {
                                autosave: autosave
                        }
@@ -28,7 +28,9 @@
                
                {* WoltLab *}
                '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabButton.js?v={@LAST_UPDATE_TIME}',
-               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabDropdown.js?v={@LAST_UPDATE_TIME}'
+               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabDropdown.js?v={@LAST_UPDATE_TIME}', 
+               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabEvent.js?v={@LAST_UPDATE_TIME}',
+               '{@$__wcf->getPath()}js/3rdParty/redactor2/plugins/WoltLabQuote.js?v={@LAST_UPDATE_TIME}'
                
                ], function() {
                        WCF.System.Dependency.Manager.invoke(callbackIdentifier);
index afd01a3093c882da86fa1c889f4402f8ba68628d..e313a9c5fcfa3ca65f653e50e776ca401315bf5d 100644 (file)
@@ -8,11 +8,11 @@ $.Redactor.prototype.WoltLabDropdown = function() {
                        this.utils.enableBodyScroll = function() {};
                        
                        // disable slideUp effect for dropdowns on close
-                       this._hideAll();
+                       this.WoltLabDropdown._hideAll();
                        
                        // disable slideDown effect for dropdowns on open
                        // enforce dropdownMenu-like DOM
-                       this._show();
+                       this.WoltLabDropdown._show();
                },
                
                _hideAll: function() {
diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabEvent.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabEvent.js
new file mode 100644 (file)
index 0000000..2dcedae
--- /dev/null
@@ -0,0 +1,22 @@
+$.Redactor.prototype.WoltLabEvent = function() {
+       "use strict";
+       
+       return {
+               init: function() {
+                       require(['EventHandler'], this.WoltLabEvent._setEvents.bind(this));
+               },
+               
+               _setEvents: function(EventHandler) {
+                       var elementId = this.$element[0].id;
+                       
+                       var observeLoad = this.observe.load;
+                       this.observe.load = (function() {
+                               observeLoad.call(this);
+                               
+                               EventHandler.fire('com.woltlab.wcf.redactor', 'observe_load_' + elementId, {
+                                       editor: this.$editor[0]
+                               });
+                       }).bind(this);
+               }
+       };
+};
diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabQuote.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabQuote.js
new file mode 100644 (file)
index 0000000..bc25002
--- /dev/null
@@ -0,0 +1,34 @@
+$.Redactor.prototype.WoltLabQuote = function() {
+       "use strict";
+       
+       return {
+               init: function() {
+                       // TODO: this should be somewhere else
+                       var button = this.button.add('woltlabQuote', 'Quote');
+                       this.button.addCallback(button, this.WoltLabQuote.insert);
+                       
+                       require(['WoltLab/WCF/Ui/Redactor/Quote'], (function(UiRedactorQuote) {
+                               UiRedactorQuote.initEditor(this.$element[0].id, this.$editor[0]);
+                       }).bind(this));
+               },
+               
+               insert: function() {
+                       require(['Dom/Traverse', 'WoltLab/WCF/Ui/Redactor/Quote'], (function(DomTraverse, UiRedactorQuote) {
+                               var current = this.selection.current();
+                               if (current) {
+                                       if (current.nodeType === Node.TEXT_NODE) current = current.parentNode;
+                                       
+                                       if (current.nodeName === 'BLOCKQUOTE' || DomTraverse.parentByTag(current, 'BLOCKQUOTE', this.$editor[0])) {
+                                               return;
+                                       }
+                               }
+                               
+                               UiRedactorQuote.insert((function(element) {
+                                       element.innerHTML = this.opts.invisibleSpace + this.selection.markerHtml();
+                                       
+                                       this.insert.html(element.outerHTML);
+                               }).bind(this));
+                       }).bind(this));
+               }
+       };
+};
index 382974c4079b92ad8edfa9bf325efc654066ab95..2c0382ff0b41ddc5653241d9f5ccb2c265f47dd6 100644 (file)
@@ -8,14 +8,14 @@
  */
 define(
        [
-               'enquire',     'Ajax',       'Core',      'Dictionary',
-               'Environment', 'Language',   'ObjectMap', 'Dom/ChangeListener',
-               'Dom/Util',    'Ui/Confirmation'
+               'enquire',      'Ajax',       'Core',      'Dictionary',
+               'Environment',  'Language',   'ObjectMap', 'Dom/ChangeListener',
+               'Dom/Traverse', 'Dom/Util',   'Ui/Confirmation'
        ],
        function(
                enquire,        Ajax,         Core,        Dictionary,
                Environment,    Language,     ObjectMap,   DomChangeListener,
-               DomUtil,        UiConfirmation
+               DomTraverse,    DomUtil,      UiConfirmation
        )
 {
        "use strict";
@@ -215,17 +215,26 @@ define(
                /**
                 * Sets the dialog title.
                 * 
-                * @param       {string}        id              element id
-                * @param       {string}        title           dialog title
+                * @param       {(string|object)}       id              element id
+                * @param       {string}                title           dialog title
                 */
                setTitle: function(id, title) {
+                       if (typeof id === 'object') {
+                               var dialogData = _dialogObjects.get(id);
+                               if (dialogData !== undefined) {
+                                       id = dialogData.id;
+                               }
+                       }
+                       
                        var data = _dialogs.get(id);
                        if (data === undefined) {
                                throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog.");
                        }
                        
-                       var header = DomTraverse.childrenByTag(data.dialog, 'HEADER');
-                       DomTraverse.childrenByTag(header[0], 'SPAN').textContent = title;
+                       var dialogTitle = elByClass('dialogTitle', data.dialog);
+                       if (dialogTitle.length) {
+                               dialogTitle[0].textContent = title;
+                       }
                },
                
                /**
@@ -258,16 +267,14 @@ define(
                        var header = elCreate('header');
                        dialog.appendChild(header);
                        
-                       if (options.title) {
-                               var titleId = DomUtil.getUniqueId();
-                               elAttr(dialog, 'aria-labelledby', titleId);
-                               
-                               var title = elCreate('span');
-                               title.classList.add('dialogTitle');
-                               title.textContent = options.title;
-                               elAttr(title, 'id', titleId);
-                               header.appendChild(title);
-                       }
+                       var titleId = DomUtil.getUniqueId();
+                       elAttr(dialog, 'aria-labelledby', titleId);
+                       
+                       var title = elCreate('span');
+                       title.classList.add('dialogTitle');
+                       title.textContent = options.title;
+                       elAttr(title, 'id', titleId);
+                       header.appendChild(title);
                        
                        if (options.closable) {
                                var closeButton = elCreate('a');
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js
new file mode 100644 (file)
index 0000000..1c7db1b
--- /dev/null
@@ -0,0 +1,193 @@
+/**
+ * Manages insertation and editing of quotes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Ui/Redactor/Quote
+ */
+define(['EventHandler', 'Language', 'Dom/Util', 'Ui/Dialog'], function(EventHandler, Language, DomUtil, UiDialog) {
+       "use strict";
+       
+       var _element = null;
+       var _insertCallback = null;
+       var _quotePaddingTop = 0;
+       var _titleHeight = 0;
+       var _wysiwygQuoteButton = null;
+       var _wysiwygQuoteTitle = null;
+       var _wysiwygQuoteUrl = null;
+       
+       var UiRedactorQuote = {
+               /**
+                * Registers an editor instance.
+                * 
+                * @param       {string}        editorId        textarea identifier
+                * @param       {object}        editor          editor element
+                */
+               initEditor: function(editorId, editor) {
+                       EventHandler.add('com.woltlab.wcf.redactor', 'observe_load_' + editorId, (function(data) {
+                               this.observeAll(data.editor);
+                       }).bind(this));
+                       
+                       this.observeAll(editor);
+               },
+               
+               /**
+                * Opens a dialog to insert a quote at caret position.
+                * 
+                * @param       {function}      callback        callback invoked with the <blockquote> element as parameter
+                */
+               insert: function(callback) {
+                       _insertCallback = callback;
+                       
+                       UiDialog.open(this);
+                       UiDialog.setTitle(this, Language.get('wcf.wysiwyg.quote.insert'));
+                       
+                       _wysiwygQuoteButton.textContent = Language.get('wcf.global.button.submit');
+                       _wysiwygQuoteTitle.value = '';
+                       _wysiwygQuoteUrl.value = '';
+               },
+               
+               /**
+                * Edits a <blockquote> element.
+                * 
+                * @param       {Element}       element         <blockquote> element
+                * @param       {Event=}        event           event object
+                */
+               edit: function(element, event) {
+                       if (_titleHeight === 0) {
+                               var styles = window.getComputedStyle(element, '::before');
+                               _titleHeight = DomUtil.styleAsInt(styles, 'height');
+                               
+                               styles = window.getComputedStyle(element);
+                               _quotePaddingTop = DomUtil.styleAsInt(styles, 'padding-top');
+                       }
+                       
+                       if (typeof event === 'object') {
+                               // check if click occured within the ::before pseudo element
+                               var rect = DomUtil.offset(element);
+                               if ((event.clientY + document.body.scrollTop) > (rect.top + _quotePaddingTop + _titleHeight)) {
+                                       return;
+                               }
+                               
+                               event.preventDefault();
+                       }
+                       
+                       _element = element;
+                       
+                       UiDialog.open(this);
+                       UiDialog.setTitle(this, Language.get('wcf.wysiwyg.quote.edit'));
+                       
+                       // set values
+                       _wysiwygQuoteButton.textContent = Language.get('wcf.global.button.save');
+                       _wysiwygQuoteTitle.value = elData(_element, 'quote-title');
+                       _wysiwygQuoteUrl.value = elData(_element, 'quote-url');
+               },
+               
+               /**
+                * Observes all <blockquote> elements for clicks on the editable headline
+                * @param       {Element}       editorElement           editor element
+                */
+               observeAll: function(editorElement) {
+                       var elements = elByTag('BLOCKQUOTE', editorElement);
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               this._observe(elements[i], true);
+                       }
+               },
+               
+               /**
+                * Observes clicks on a <blockquote> element and updates the headline.
+                * 
+                * @param       {Element}       element         <blockquote> element
+                * @param       {boolean}       updateHeader    update quote header
+                */
+               _observe: function(element, updateHeader) {
+                       element.addEventListener('click', this.edit.bind(this, element));
+                       
+                       if (updateHeader) this._updateHeader(element);
+               },
+               
+               /**
+                * Updates the headline of target <blockquote> element.
+                * 
+                * @param       {Element}       element         <blockquote> element
+                */
+               _updateHeader: function(element) {
+                       elData(element, 'quote-header', Language.get('wcf.wysiwyg.quote.header', {
+                               title: elData(element, 'quote-title') || elData(element, 'quote-url') || ''
+                       }));
+               },
+               
+               /**
+                * Adds or edits a <blockquote> element on dialog submit.
+                */
+               _dialogSubmit: function() {
+                       if (_insertCallback !== null) {
+                               // insert a new <blockquote> element
+                               var element = elCreate('blockquote');
+                               element.className = 'quoteBox';
+                               element.id = 'quote-' + DomUtil.getUniqueId();
+                               
+                               _insertCallback(element);
+                               
+                               _element = elById(element.id);
+                               _element.id = '';
+                               
+                               this._observe(_element, false);
+                       }
+                       
+                       // edit an existing <blockquote> element
+                       elData(_element, 'quote-title', _wysiwygQuoteTitle.value.trim());
+                       elData(_element, 'quote-url', _wysiwygQuoteUrl.value.trim());
+                       
+                       this._updateHeader(_element);
+                       
+                       UiDialog.close(this);
+               },
+               
+               _dialogOnSetup: function() {
+                       _wysiwygQuoteTitle = elById('wysiwygQuoteTitle');
+                       _wysiwygQuoteUrl = elById('wysiwygQuoteUrl');
+                       
+                       var _keyupCallback = (function(event) {
+                               if (event.which === 13) {
+                                       this._dialogSubmit(event);
+                               }
+                       }).bind(this);
+                       
+                       _wysiwygQuoteTitle.addEventListener('keyup', _keyupCallback);
+                       _wysiwygQuoteUrl.addEventListener('keyup', _keyupCallback);
+                       
+                       _wysiwygQuoteButton = elById('wysiwygQuoteSubmit');
+                       _wysiwygQuoteButton.addEventListener('click', this._dialogSubmit.bind(this));
+               },
+               
+               _dialogOnClose: function() {
+                       _element = null;
+                       _insertCallback = null;
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'wysiwygQuoteDialog',
+                               options: {
+                                       onClose: this._dialogOnClose.bind(this),
+                                       onSetup: this._dialogOnSetup.bind(this)
+                               },
+                               source: '<dl>'
+                                               + '<dt><label for="wysiwygQuoteTitle">' + Language.get('wcf.wysiwyg.quote.title') + '</label></dt>'
+                                               + '<dd><input type="text" id="wysiwygQuoteTitle" class="long"></dd>'
+                                       + '</dl>'
+                                       + '<dl>'
+                                               + '<dt><label for="wysiwygQuoteUrl">' + Language.get('wcf.wysiwyg.quote.url') + '</label></dt>'
+                                               + '<dd><input type="text" id="wysiwygQuoteUrl" class="long"></dd>'
+                                       + '</dl>'
+                                       + '<div class="formSubmit">'
+                                               + '<button class="buttonPrimary" id="wysiwygQuoteSubmit"></button>'
+                                       + '</div>'
+                       };
+               }
+       };
+       
+       return UiRedactorQuote;
+});
diff --git a/wcfsetup/install/files/style/bbcode/quote.less b/wcfsetup/install/files/style/bbcode/quote.less
deleted file mode 100644 (file)
index 3f17b70..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-.quoteBox {
-       border-left: 2px solid rgb(79, 129, 189);
-       padding: 10px;
-       margin: 10px 0;
-       
-       &:first-child {
-               margin-top: 0;
-       }
-       
-       .quoteBox {
-               margin-left: 10px;
-               
-               > .quoteBoxHeader > .quoteBoxIcon {
-                       display: none;
-               }
-       }
-}
-
-.quoteBoxHeader {
-       align-items: center;
-       display: flex;
-       margin-bottom: 10px;
-}
-
-.quoteBoxIcon {
-       flex: 0 0 auto;
-       min-height: 36px;
-       
-       > a {
-               display: block;
-               font-size: 0;
-               margin-right: 5px;
-       }
-       
-       > .icon {
-               margin-right: 7px;
-               position: relative;
-               top: 10px;
-       }
-}
-
-.quoteBoxTitle {
-       flex: 1 auto;
-       font-size: 1.2rem;
-}
diff --git a/wcfsetup/install/files/style/bbcode/quote.scss b/wcfsetup/install/files/style/bbcode/quote.scss
new file mode 100644 (file)
index 0000000..2a09d13
--- /dev/null
@@ -0,0 +1,46 @@
+.quoteBox {
+       border-left: 2px solid $wcfContentBorder;
+       padding: 10px;
+       margin: 10px 0;
+       
+       &:first-child {
+               margin-top: 0;
+       }
+       
+       .quoteBox {
+               margin-left: 10px;
+               
+               > .quoteBoxHeader > .quoteBoxIcon {
+                       display: none;
+               }
+       }
+}
+
+.quoteBoxHeader {
+       align-items: center;
+       display: flex;
+       margin-bottom: 10px;
+}
+
+.quoteBoxIcon {
+       flex: 0 0 auto;
+       min-height: 36px;
+       
+       > a {
+               display: block;
+               font-size: 0;
+               margin-right: 5px;
+       }
+       
+       > .icon {
+               margin-right: 7px;
+               position: relative;
+               top: 10px;
+       }
+}
+
+.quoteBoxTitle {
+       flex: 1 auto;
+       
+       @extend .wcfFontHeadline;
+}
index be38b859e50b053265a08db479295e4e4827909a..c8874ddec121c452520f5ba127d012be82ae21cd 100644 (file)
                }
        }
        
-       .TODO_quoteBox {
-               clear: none;
+       .quoteBox {
+               position: relative;
                
-               > header {
-                       position: relative;
+               &::before {
+                       content: attr(data-quote-header);
+                       cursor: pointer;
+                       display: block;
+                       margin-bottom: 10px;
+                       padding-right: 20px;
                        
-                       > .redactorQuoteEdit {
-                               bottom: -$wcfGapSmall;
-                               left: -$wcfGapMedium;
-                               right: -$wcfGapMedium;
-                               top: -$wcfGapMedium;
-                               padding-top: $wcfGapMedium;
-                               padding-left: $wcfGapMedium;
-                               position: absolute;
-                       }
+                       @extend .wcfFontHeadline;
+               }
+               
+               &::after {
+                       content: $fa-var-pencil;
+                       font-family: FontAwesome;
+                       font-size: $wcfFontSizeHeadline;
+                       position: absolute;
+                       right: 10px;
+                       top: 10px;
                }
        }