work in progress: visual representation of quotes
authorAlexander Ebert <ebert@woltlab.com>
Sat, 2 Aug 2014 19:03:05 +0000 (21:03 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 2 Aug 2014 19:03:05 +0000 (21:03 +0200)
com.woltlab.wcf/templates/wysiwyg.tpl
wcfsetup/install/files/js/3rdParty/redactor/plugins/wbbcode.js
wcfsetup/install/files/js/3rdParty/redactor/plugins/wmonkeypatch.js
wcfsetup/install/files/style/redactor.less
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 3fda90d2b784c57e6c6e39f9c36e6dd104eb95d3..2c0b67007e2dcfa3e4ce044db613bd8fa037e012 100644 (file)
@@ -6,6 +6,16 @@ var __REDACTOR_SOURCE_BBCODES = [ {implode from=$__wcf->getBBCodeHandler()->getS
 </script>
 <script data-relocate="true">
 $(function() {
+       WCF.Language.addObject({
+               'wcf.attachment.dragAndDrop.dropHere': '{lang}wcf.attachment.dragAndDrop.dropHere{/lang}',
+               'wcf.attachment.dragAndDrop.dropNow': '{lang}wcf.attachment.dragAndDrop.dropNow{/lang}',
+               'wcf.bbcode.quote.edit': '{lang}wcf.bbcode.quote.edit{/lang}',
+               'wcf.bbcode.quote.edit.author': '{lang}wcf.bbcode.quote.edit.author{/lang}',
+               'wcf.bbcode.quote.edit.link': '{lang}wcf.bbcode.quote.edit.link{/lang}',
+               'wcf.bbcode.quote.title.clickToSet': '{lang}wcf.bbcode.quote.title.clickToSet{/lang}',
+               'wcf.bbcode.quote.title.javascript': '{lang}wcf.bbcode.quote.title.javascript{/lang}'
+       });
+       
        var $editorName = '{if $wysiwygSelector|isset}{$wysiwygSelector|encodeJS}{else}text{/if}';
        var $callbackIdentifier = 'Redactor_' + $editorName;
        
@@ -18,6 +28,7 @@ $(function() {
                var $autosave = $textarea.data('autosave');
                var $config = {
                        buttons: $buttons,
+                       convertDivs: false,
                        convertImageLinks: false,
                        convertLinks: false,
                        convertVideoLinks: false,
@@ -35,11 +46,6 @@ $(function() {
                };
                
                {if MODULE_ATTACHMENT && !$attachmentHandler|empty && $attachmentHandler->canUpload()}
-                       WCF.Language.addObject({
-                               'wcf.attachment.dragAndDrop.dropHere': '{lang}wcf.attachment.dragAndDrop.dropHere{/lang}',
-                               'wcf.attachment.dragAndDrop.dropNow': '{lang}wcf.attachment.dragAndDrop.dropNow{/lang}'
-                       });
-                       
                        $config.plugins.push('wupload');
                        $config.wAttachmentUrl = '{link controller='Attachment' id=987654321}thumbnail=1{/link}';
                {/if}
index 1b7ae923ffa860c0adbb82b16bd720bdbb745bb8..f70ba4b943b09a3c7fee16eb1843de926aafd711 100644 (file)
@@ -27,6 +27,8 @@ RedactorPlugins.wbbcode = {
                this.opts.pasteBeforeCallback = $.proxy(this._wPasteBeforeCallback, this);
                this.opts.pasteAfterCallback = $.proxy(this._wPasteAfterCallback, this);
                
+               this.opts.keydownCallback = $.proxy(this._wKeydownCallback, this);
+               
                var $mpSyncClean = this.syncClean;
                var self = this;
                this.syncClean = function(html) {
@@ -144,6 +146,7 @@ RedactorPlugins.wbbcode = {
                else {
                        this._convertToHtml();
                        this.toggleVisual();
+                       this._observeQuotes();
                        
                        this.buttonGet('html').children('i').removeClass('fa-square').addClass('fa-square-o');
                }
@@ -244,8 +247,18 @@ RedactorPlugins.wbbcode = {
                html = html.replace(/<img [^>]*?src=(["'])([^"']+?)\1.*?>/gi, '[img]$2[/img]');
                
                // [quote]
-               // html = html.replace(/<blockquote>/gi, '[quote]');
-               // html = html.replace(/\n*<\/blockquote>/gi, '[/quote]');
+               html = html.replace(/<blockquote class="quoteBox" cite="([^"]+)?" data-author="([^"]+)?">/gi, function(match, link, author) {
+                       if (link) {
+                               return "[quote='" + author + "','" + link + "']";
+                       }
+                       else if (author) {
+                               return "[quote='" + author + "']";
+                       }
+                       
+                       return "[quote]";
+               });
+               html = html.replace(/<\/blockquote>/gi, '[/quote]');
+               html = html.replace(/<header data-ignore="true">.*?<\/header>/gi, '');
                
                // handle [color], [size], [font] and [tt]
                var $components = html.split(/(<\/?span[^>]*>)/);
@@ -414,6 +427,11 @@ RedactorPlugins.wbbcode = {
                }
                html = $tmp.join("\n");
                
+               // trim whitespaces within quote tags
+               html = html.replace(/\[quote([^\]]+)?\](.*?)\[\/quote\]/, function(match, attributes, content) {
+                       return '[quote' + attributes + ']' + $.trim(content) + '[/quote]';
+               });
+               
                // insert codes
                if ($.getLength($cachedCodes)) {
                        for (var $key in $cachedCodes) {
@@ -486,10 +504,6 @@ RedactorPlugins.wbbcode = {
                data = data.replace(/\[img='?([^"]*?)'?,'?(left|right)'?\]\[\/img\]/gi,'<img src="$1" style="float: $2" />');
                data = data.replace(/\[img='?([^"]*?)'?\]\[\/img\]/gi,'<img src="$1" />');
                
-               // [quote]
-               // data = data.replace(/\[quote\]/gi, '<blockquote>');
-               // data = data.replace(/\[\/quote\]/gi, '</blockquote>');
-               
                // [size]
                data = data.replace(/\[size=(\d+)\](.*?)\[\/size\]/gi,'<span style="font-size: $1pt">$2</span>');
                
@@ -599,6 +613,53 @@ RedactorPlugins.wbbcode = {
                // preserve leading whitespaces in [code] tags
                data = data.replace(/\[code\][\S\s]*?\[\/code\]/, '<pre>$&</pre>');
                
+               // [quote]
+               var $unquoteString = function(quotedString) {
+                       return quotedString.replace(/^['"]/, '').replace(/['"]$/, '');
+               };
+               
+               data = data.replace(/\[quote([^\]]+)?\]/gi, function(match, attributes) {
+                       var $quote = '<blockquote class="quoteBox" cite="" data-author="">';
+                       
+                       if (attributes) {
+                               attributes = attributes.substr(1);
+                               attributes = attributes.split(',');
+                               var $author = '';
+                               var $link = '';
+                               
+                               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)));
+                               
+                               $quote = '<blockquote class="quoteBox" cite="' + $link + '" data-author="' + $author + '">';
+                               $quote += '<div class="container containerPadding">'
+                                               + '<header data-ignore="true">'
+                                                       + '<h3>'
+                                                               + ($link ? '<a href="' + $link + '">' : '') + WCF.Language.get('wcf.bbcode.quote.title.javascript', { quoteAuthor: WCF.String.unescapeHTML($author) }) + ($link ? '</a>' : '')
+                                                       + '</h3>'
+                                                       + '<a class="redactorQuoteEdit"></a>'
+                                               + '</header>';
+                       }
+                       
+                       $quote += '<div>';
+                       
+                       return $quote;
+               });
+               data = data.replace(/\[\/quote\]/gi, '</div></div></blockquote>');
+               
+               data = data.replace(/<p><blockquote/gi, '<blockquote');
+               data = data.replace(/<\/blockquote><\/p>/, '</blockquote>');//<p>' + this.getOption('invisibleSpace') + '</p>');
+               
                this.$source.val(data);
        },
        
@@ -715,5 +776,126 @@ RedactorPlugins.wbbcode = {
                WCF.System.Event.fireEvent('com.woltlab.wcf.redactor', 'getImageAttachments_' + this.$source.wcfIdentify(), $data);
                
                return $data.imageAttachmentIDs;
+       },
+       
+       _wKeydownCallback: function(event) {
+               if (event.which === this.keyCode.DOWN) {
+                       var $parent = this.getParent();
+                       var $quote = ($parent) ? $($parent).closest('blockquote.quoteBox', this.$editor.get()[0]) : { length: 0 };
+                       
+                       if ($parent && $quote.length) {
+                               this.insertAfterLastElement($(parent)[0], $quote[0]);
+                       }
+                       
+                       return false;
+               }
+               
+               return true;
+       },
+       
+       _observeQuotes: function() {
+               this.$editor.find('.redactorQuoteEdit:not(.jsRedactorQuoteEdit)').addClass('jsRedactorQuoteEdit').click($.proxy(this._observeQuotesClick, this));
+       },
+       
+       _observeQuotesClick: function(event) {
+               var $header = $(event.currentTarget).closest('header');
+               var $tooltip = $('<span class="redactor-link-tooltip" />');
+               
+               $('<a href="#">' + WCF.Language.get('wcf.bbcode.quote.edit') + '</a>').click($.proxy(function(e) {
+                       e.preventDefault();
+                       
+                       this._openQuoteEditOverlay($(event.currentTarget).closest('blockquote.quoteBox'));
+                       $('.redactor-link-tooltip').remove();
+               }, this)).appendTo($tooltip);
+               
+               var $offset = $header.offset();
+               $tooltip.css({
+                       left: $offset.left + 'px',
+                       top: ($offset.top + 20) + 'px'
+               });
+               
+               $('.redactor-link-tooltip').remove();
+               $tooltip.appendTo(document.body);
+       },
+       
+       _openQuoteEditOverlay: function(quote) {
+               this.modalInit(WCF.Language.get('wcf.bbcode.quote.edit'), this.opts.modal_quote, 300, $.proxy(function() {
+                       $('#redactorQuoteAuthor').val(quote.data('author'));
+                       
+                       // do not use prop() here, an empty cite attribute would yield the page URL instead
+                       $('#redactorQuoteLink').val(quote.attr('cite'));
+                       
+                       $('#redactorEditQuote').click($.proxy(function() {
+                               var $author = $('#redactorQuoteAuthor').val();
+                               quote.data('author', $author);
+                               quote.attr('data-author', $author);
+                               quote.prop('cite', WCF.String.escapeHTML($('#redactorQuoteLink').val()));
+                               
+                               this._updateQuoteHeader(quote);
+                               
+                               this.modalClose();
+                       }, this));
+               }, this));
+       },
+       
+       _updateQuoteHeader: function(quote) {
+               var $author = quote.data('author');
+               var $link = quote.attr('cite');
+               if ($link) $link = WCF.String.escapeHTML($link);
+               
+               quote.find('> div > header > h3').empty().append(this._buildQuoteHeader($author, $link));       
+       },
+       
+       insertQuoteBBCode: function(author, link) {
+               if (this.inWysiwygMode()) {
+                       var $html = '<blockquote class="quoteBox" cite="' + $link + '" data-author="' + $author + '">'
+                                       + '<div class="container containerPadding">'
+                                               + '<header data-ignore="true">'
+                                                       + '<h3>'
+                                                               + this._buildQuoteHeader(author, link)
+                                                       + '</h3>'
+                                                       + '<a class="redactorQuoteEdit"></a>'
+                                               + '</header>';
+                                               + '<div id="redactorInsertedQuote">' + this.opts.invisibleSpace + '</div>'
+                                       + '</div>'
+                               + '</blockquote>';
+                       
+                       this.insertHtml($html);
+               }
+               else {
+                       var $bbcode = '[quote][/quote]';
+                       if (author) {
+                               if (link) {
+                                       $bbcode = "[quote='" + author + "','" + link + "']";
+                               }
+                               else {
+                                       $bbcode = "[quote='" + author + "'][/quote]";
+                               }
+                       }
+                       
+                       this.insertAtCaret($bbcode);
+               }
+       },
+       
+       _buildQuoteHeader: function(author, link) {
+               var $header = '';
+               // author is empty, check if link was provided and use it instead
+               if (!author && link) {
+                       author = link;
+                       link = '';
+               }
+               
+               if (author) {
+                       if (link) $header += '<a href="' + link + '">';
+                       
+                       $header += WCF.Language.get('wcf.bbcode.quote.title.javascript', { quoteAuthor: WCF.String.unescapeHTML(author) });
+                       
+                       if (link) $header += '</a>';
+               }
+               else {
+                       $header = '<small>' + WCF.Language.get('wcf.bbcode.quote.title.clickToSet') + '</small>';
+               }
+               
+               return $header;
        }
 };
index bf6d562f96549efc520931b4e7dd316dd532a9bc..4f6a06d60163cb4e526d7d56e800b050dc69841e 100644 (file)
@@ -308,39 +308,21 @@ RedactorPlugins.wmonkeypatch = {
                        + '</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>',
-                       // img edit
-                       
-                       // img
-
-                       // link
-                       
-                       // table
-                       
-                       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>'
-
-               });
+               this.setOption('modal_quote',
+                       '<fieldset>'
+                               + '<dl>'
+                                       + '<dt><label for="redactorQuoteAuthor">' + WCF.Language.get('wcf.bbcode.quote.edit.author') + '</label></dt>'
+                                       + '<dd><input type="text" id="redactorQuoteAuthor" class="long" /></dd>'
+                               + '</dl>'
+                               + '<dl>'
+                                       + '<dt><label for="redactorQuoteLink">' + WCF.Language.get('wcf.bbcode.quote.edit.link') + '</label></dt>'
+                                       + '<dd><input type="text" id="redactorQuoteLink" class="long" /></dd>'
+                               + '</dl>'
+                       + '</fieldset>'
+                       + '<div class="formSubmit">'
+                               + '<button id="redactorEditQuote">' + this.opts.curLang.save + '</button>'
+                       + '</div>'
+               );
        },
        
        mpModalInit: function() {
@@ -453,5 +435,19 @@ RedactorPlugins.wmonkeypatch = {
                else {
                        this.modalClose();
                }
-       }
+       },
+       
+       observeLinks: function() {
+               this.$editor.find('a:not(.redactorQuoteEdit)').on('click', $.proxy(this.linkObserver, this));
+               
+               this.$editor.on('click.redactor', $.proxy(function(e)
+               {
+                       this.linkObserverTooltipClose(e);
+               }, this));
+               
+               $(document).on('click.redactor', $.proxy(function(e)
+               {
+                       this.linkObserverTooltipClose(e);
+               }, this));
+       },
 };
index 74dc887af4d8e4c3d01b8d68de1d0cb4ed748475..cfd9f60a32573c5c1cfd61d3e375e2e9da0a6d84 100644 (file)
                        vertical-align: top;
                }
        }
+       
+       blockquote > div > header {
+               position: relative;
+               
+               > .redactorQuoteEdit {
+                       bottom: -7px;
+                       left: -14px;
+                       right: -14px;
+                       top: -14px;
+                       padding-top: 14px;
+                       padding-left: 14px;
+                       position: absolute;
+               }
+       }
 }
 
 .redactor_toolbar {
                border-color: #04B404;
        }
 }
+
+.redactor-link-tooltip {
+       background-color: @wcfTooltipBackgroundColor;
+       border-radius: 6px;
+       color: @wcfTooltipColor;
+       font-size: @wcfSmallFontSize;
+       padding: 5px 10px 7px;
+       position: absolute;
+       z-index: 800;
+       
+       .boxShadow(0, 3px, rgba(0, 0, 0, .3), 7px);
+       
+       > a {
+               color: @wcfTooltipColor;
+       }
+}
index 49d6c4dc5b2a99faab5d021669a5404b059cc6c7..ce4a0ade444e19e69a936c0feeacd1d1380e7790 100644 (file)
@@ -1689,7 +1689,12 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt
                <item name="wcf.bbcode.code.tex.title"><![CDATA[TeX-Quellcode]]></item>
                <item name="wcf.bbcode.code.tpl.title"><![CDATA[Smarty-Template]]></item>
                <item name="wcf.bbcode.code.xml.title"><![CDATA[XML-Quellcode]]></item>
+               <item name="wcf.bbcode.quote.edit"><![CDATA[Zitat bearbeiten]]></item>
+               <item name="wcf.bbcode.quote.edit.author"><![CDATA[Autor]]></item>
+               <item name="wcf.bbcode.quote.edit.link"><![CDATA[Quelle]]></item>
                <item name="wcf.bbcode.quote.title"><![CDATA[{@$quoteAuthor} schrieb:]]></item>
+               <item name="wcf.bbcode.quote.title.clickToSet"><![CDATA[(Klicken um eine Quelle anzugeben)]]></item>
+               <item name="wcf.bbcode.quote.title.javascript"><![CDATA[{literal}{$quoteAuthor} schrieb:{/literal}]]></item>
                <item name="wcf.bbcode.quote.text"><![CDATA[Zitat{if $cite} von {@$cite}{/if}: „{@$content}“]]></item>
                <item name="wcf.bbcode.spoiler.hide"><![CDATA[Spoiler ausblenden]]></item>
                <item name="wcf.bbcode.spoiler.show"><![CDATA[Spoiler anzeigen]]></item>
index 6467b5c46f14f41cd8931744f21a8a216e1416ca..329fc7ca589e3368f31d5a0f551bf96df209beaa 100644 (file)
@@ -1656,7 +1656,12 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi
                <item name="wcf.bbcode.code.tex.title"><![CDATA[TeX Source Code]]></item>
                <item name="wcf.bbcode.code.tpl.title"><![CDATA[Smarty-Template]]></item>
                <item name="wcf.bbcode.code.xml.title"><![CDATA[XML Source Code]]></item>
+               <item name="wcf.bbcode.quote.edit"><![CDATA[Edit Quote]]></item>
+               <item name="wcf.bbcode.quote.edit.author"><![CDATA[Author]]></item>
+               <item name="wcf.bbcode.quote.edit.link"><![CDATA[Source]]></item>
                <item name="wcf.bbcode.quote.title"><![CDATA[{@$quoteAuthor} wrote:]]></item>
+               <item name="wcf.bbcode.quote.title.clickToSet"><![CDATA[(Click to set source)]]></item>
+               <item name="wcf.bbcode.quote.title.javascript"><![CDATA[{literal}{$quoteAuthor} wrote:{/literal}]]></item>
                <item name="wcf.bbcode.quote.text"><![CDATA[Quote{if $cite} from {@$cite}{/if}: “{@$content}”]]></item>
                <item name="wcf.bbcode.spoiler.hide"><![CDATA[Hide Spoiler]]></item>
                <item name="wcf.bbcode.spoiler.show"><![CDATA[Show Spoiler]]></item>