From: Alexander Ebert Date: Mon, 2 Nov 2015 14:09:18 +0000 (+0100) Subject: Added quote support, still getting used to Redactor II X-Git-Tag: 3.0.0_Beta_1~2030^2~254 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=f2351ad6d60a89a3c5427f5b01be416e1509ec63;p=GitHub%2FWoltLab%2FWCF.git Added quote support, still getting used to Redactor II --- diff --git a/com.woltlab.wcf/templates/wysiwyg.tpl b/com.woltlab.wcf/templates/wysiwyg.tpl index da1773e6f7..475c223556 100644 --- a/com.woltlab.wcf/templates/wysiwyg.tpl +++ b/com.woltlab.wcf/templates/wysiwyg.tpl @@ -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); diff --git a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabDropdown.js b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabDropdown.js index afd01a3093..e313a9c5fc 100644 --- a/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabDropdown.js +++ b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabDropdown.js @@ -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 index 0000000000..2dcedae7dd --- /dev/null +++ b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabEvent.js @@ -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 index 0000000000..bc2500245b --- /dev/null +++ b/wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabQuote.js @@ -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)); + } + }; +}; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js index 382974c407..2c0382ff0b 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js @@ -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 index 0000000000..1c7db1bdf7 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js @@ -0,0 +1,193 @@ +/** + * Manages insertation and editing of quotes. + * + * @author Alexander Ebert + * @copyright 2001-2015 WoltLab GmbH + * @license GNU Lesser General Public License + * @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
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
element. + * + * @param {Element} element
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
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
element and updates the headline. + * + * @param {Element} element
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
element. + * + * @param {Element} element
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
element on dialog submit. + */ + _dialogSubmit: function() { + if (_insertCallback !== null) { + // insert a new
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
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: '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + }; + } + }; + + return UiRedactorQuote; +}); diff --git a/wcfsetup/install/files/style/bbcode/quote.less b/wcfsetup/install/files/style/bbcode/quote.less deleted file mode 100644 index 3f17b705a2..0000000000 --- a/wcfsetup/install/files/style/bbcode/quote.less +++ /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 index 0000000000..2a09d13829 --- /dev/null +++ b/wcfsetup/install/files/style/bbcode/quote.scss @@ -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; +} diff --git a/wcfsetup/install/files/style/ui/redactor.scss b/wcfsetup/install/files/style/ui/redactor.scss index be38b859e5..c8874ddec1 100644 --- a/wcfsetup/install/files/style/ui/redactor.scss +++ b/wcfsetup/install/files/style/ui/redactor.scss @@ -101,21 +101,26 @@ } } - .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; } }