[Rt-commit] rt branch 5.0/quoting-text-with-ckeditor2 created. rt-5.0.4-32-g546c3f8a9a

BPS Git Server git at git.bestpractical.com
Fri Jun 16 22:13:05 UTC 2023


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 5.0/quoting-text-with-ckeditor2 has been created
        at  546c3f8a9a80dc8fe563a26502887ff1bd317cc2 (commit)

- Log -----------------------------------------------------------------
commit 546c3f8a9a80dc8fe563a26502887ff1bd317cc2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jun 1 23:57:44 2023 +0300

    Tweak quoted selection content and quote it with blockquote for html
    
    Previously quoted content was always plain text based, which lost
    original html structure. This commit keeps the original html and passes
    it to RT server and let the RT server quote it appropriately.

diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index 9f0909294a..1b764325cd 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -325,25 +325,37 @@ sub HasContent {
 }
 
 
-
 =head2 Content PARAMHASH
 
-If this transaction has attached mime objects, returns the body of the first
-textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
-returns the message "This transaction appears to have no content".
+Returns the content of this transaction as a string. Optionally quoted. When transaction
+has no content, returns the message "This transaction appears to have no content". Read
+L<ContentObj> to understand how the content is determined.
+
+Arguments:
+
+=over 4
+
+=item Type - type of the content to return, either C<text/plain> or C<text/html>
+
+=item Quote - quote the content and prepends it with the L</QuoteHeader>
+
+=item Wrap - size at which to wrap the content in case of C<text/plain>.
 
-Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message
-at $args{'Wrap'}.  $args{'Wrap'} is set from the $QuoteWrapWidth
-config variable.
+=back
+
+Examples:
+
+    # returns html content of the transaction
+    my $content = $txn->Content( Type => 'text/html' );
 
-If $args{'Type'} is set to C<text/html>, this will return an HTML 
-part of the message, if available.  Otherwise it looks for a text/plain
-part. If $args{'Type'} is missing, it defaults to the value of 
-C<$RT::Transaction::PreferredContentType>, if that's missing too, 
-defaults to textual.
+    # returns plain text content of the transaction
+    my $content = $txn->Content( Type => 'text/plain' );
 
-All the MIME attachments are excluded here, because it doesn't make much sense
-to use them as transaction content.
+    # returns html content of the transaction, quoted
+    my $content = $txn->Content( Type => 'text/html', Quote => 1 );
+
+    # returns text content of the transaction, quoted and wrapped at 60 characters
+    my $content = $txn->Content( Type => 'text/plain', Quote => 1, Wrap => 60 );
 
 =cut
 
@@ -352,7 +364,6 @@ sub Content {
     my %args = (
         Type => $PreferredContentType || '',
         Quote => 0,
-        Wrap => RT->Config->Get('QuoteWrapWidth') || 70,
         @_
     );
 
@@ -399,17 +410,83 @@ sub Content {
     }
 
     if ( $args{'Quote'} ) {
-        if ($args{Type} eq 'text/html') {
-            $content = '<div class="gmail_quote">'
-                . $self->QuoteHeader
-                . '<br /><blockquote class="gmail_quote" type="cite">'
-                . $content
-                . '</blockquote></div>';
-        } else {
-            $content = $self->ApplyQuoteWrap(content => $content,
-                                             cols    => $args{'Wrap'} );
+        $content = $self->QuoteContent(
+            %args,
+            Content => $content,
+            $args{Quote} ? ( QuoteHeader => $self->QuoteHeader ) : (),
+        );
+    }
+
+    return ($content);
+}
+
+
+=head2 QuoteContent
+
+B<Class method> utility. Takes content, its type, options and returns the content,
+quoted, wrapped and with header if aked.
+
+Takes a paramhash.
+
+=over 4
+
+=item Type - type of the content, either 'text/html' or 'text/plain' (default)
+
+=item Content - the content to work on
+
+=item QuoteHeader - string, header to prepend to the content if C<Quote> is true
 
-            $content = $self->QuoteHeader . "\n$content";
+=item Wrap - integer, wrap the content at this many characters, defaults to C<QuoteWrapWidth>
+option from RT config or 70 if it's not set. Applies B<only> if expected content type is C<text/plain>.
+
+=back
+
+Example:
+
+    # quote some html content as text/html
+    my $res = RT::Transaction->QuoteContent(
+        Type        => 'text/html',
+        Content     => '<p>Hello, world!</p>',
+        QuoteHeader => 'John Doe wrote:',
+    );
+
+=cut
+
+sub QuoteContent {
+    my $self = shift;
+    my %args = (
+        Type        => $PreferredContentType || '',
+        Content     => '',
+        QuoteHeader => '',
+        Wrap        => RT->Config->Get('QuoteWrapWidth') || 70,
+        @_
+    );
+
+    my $type    = lc( $args{Type} || '' );
+    my $content = $args{Content} || '';
+
+    return '' unless $content;
+
+    if ( $type eq 'text/html' ) {
+        $content
+            = '<blockquote class="gmail_quote" type="cite">'
+            . $content
+            . '</blockquote>';
+    } else {
+        $content = $self->ApplyQuoteWrap(
+            content => $content,
+            cols    => $args{'Wrap'},
+        );
+    }
+
+    if ( $args{'QuoteHeader'} ) {
+        if ($type eq 'text/html') {
+            $content =
+                '<div class="gmail_quote">'
+                . $args{'QuoteHeader'} .'<br />'. $content
+                . '</div>';
+        } else {
+            $content = $args{'QuoteHeader'} . "\n". $content;
         }
     }
 
@@ -532,7 +609,21 @@ sub Addresses {
 
 =head2 ContentObj 
 
-Returns the RT::Attachment object which contains the content for this Transaction
+Returns the L<RT::Attachment> object which contains the content for this Transaction.
+
+Takes C<Type> argument, which can be either C<text/plain> or C<text/html>, and defines
+preferred content type. If C<Type> is set to C<text/html>, this will return an HTML
+part of the message, if available. Otherwise it looks for a C<text/plain>
+part. If the argument is missing, it defaults to the value of
+C<$RT::Transaction::PreferredContentType>.
+
+If there is no attachement of the preferred content type, returns the first textual part
+(as defined in L<RT::I18N::IsTextualContentType>).
+
+All the attachments with filenames or marked as attached files are excluded here,
+because it doesn't make much sense to use them as transaction content.
+
+Returns undef if there is no attachment matching these rules.
 
 =cut
 
diff --git a/share/html/Elements/MessageBox b/share/html/Elements/MessageBox
index b5c6677348..5850fc87c8 100644
--- a/share/html/Elements/MessageBox
+++ b/share/html/Elements/MessageBox
@@ -80,7 +80,22 @@ my $message = '';
 if ( $QuoteTransaction ) {
     my $transaction = RT::Transaction->new( $session{'CurrentUser'} );
     $transaction->Load( $QuoteTransaction );
-    $message = $transaction->Content( Quote => 1, Type  => $Type );
+
+    if ( $transaction->Id && !$QuoteContent ) {
+        $message = $transaction->Content( Quote => 1, Type => $Type );
+    } else {
+        $message = RT::Transaction->QuoteContent(
+            Type        => $Type,
+            Content     => $QuoteContent,
+            $transaction->Id ? ( QuoteHeader => $transaction->QuoteHeader ) : (),
+        );
+    }
+}
+elsif ( $QuoteContent ) {
+    $message = RT::Transaction->QuoteContent(
+        Type        => $Type,
+        Content     => $QuoteContent,
+    );
 }
 
 my $signature = $session{'CurrentUser'}->UserObj->Signature // "";
@@ -145,6 +160,7 @@ if ($Width) {
 </%INIT>
 <%ARGS>
 $QuoteTransaction          => undef
+$QuoteContent              => undef
 $Name                      => 'Content'
 $Default                   => ''
 $Width                     => RT->Config->Get('MessageBoxWidth', $session{'CurrentUser'} )
diff --git a/share/static/js/quoteselection.js b/share/static/js/quoteselection.js
index 48e01a7557..4512985c32 100644
--- a/share/static/js/quoteselection.js
+++ b/share/static/js/quoteselection.js
@@ -1,79 +1,113 @@
 jQuery(function() {
-    if(RT.Config.QuoteSelectedText) {
-        var reply_from_selection = function(ev) {
-            var link = jQuery(this);
-
-            var selection;
-            var activeElement;
-            if (window.getSelection) {
-                selection = window.getSelection();
-            } else {
-                return;
-            }
+    if(!RT.Config.QuoteSelectedText) {
+        return;
+    }
 
-            if (selection.rangeCount) {
-                activeElement = selection.getRangeAt(0);
-            } else {
-                return;
+    var add_sub_container = function (container, tagName) {
+        var sub = document.createElement(tagName);
+        container.appendChild(sub);
+        return sub;
+    };
+
+    var range_html = function (range) {
+        var topContainer = document.createElement('div');
+        var container = topContainer;
+
+        var fragment = range.cloneContents();
+
+        var child = fragment.firstElementChild;
+        if (child) {
+            var tn = child.tagName;
+            if (tn == "LI") {
+                container = add_sub_container(container, 'ul');
+            } else if (tn == "DT" || tn == "DD") {
+                container = add_sub_container(container, 'dl');
+            } else if (tn == "TD" || tn == "TH") {
+                container = add_sub_container(container, 'table');
+                container = add_sub_container(container, 'tbody');
+                container = add_sub_container(container, 'tr');
+            } else if (tn == "TR") {
+                container = add_sub_container(container, 'table');
+                container = add_sub_container(container, 'tbody');
+            } else if (tn == "TBODY" || tn == "THEAD" || tn == "TFOOT") {
+                container = add_sub_container(container, 'table');
             }
+        }
 
-            // check if selection has commonAncestorContainer with class 'messagebody'
-            var commonAncestor = activeElement.commonAncestorContainer;
-            if (commonAncestor) {
-                var isMessageBody = false;
-                var parent = commonAncestor.parentNode;
-                while (parent) {
-                    if (parent.className && parent.className.indexOf('messagebody') != -1) {
-                        isMessageBody = true;
-                        break;
-                    }
-                    parent = parent.parentNode;
-                }
-                if (!isMessageBody) {
-                    return;
+        container.appendChild(fragment);
+        return topContainer.innerHTML;
+    };
+
+    var reply_from_selection = function(ev) {
+        var link = jQuery(this);
+
+        var selection;
+        var activeElement;
+        if (window.getSelection) {
+            selection = window.getSelection();
+        } else {
+            return;
+        }
+
+        if (selection.rangeCount) {
+            activeElement = selection.getRangeAt(0);
+        } else {
+            return;
+        }
+
+        // check if selection has commonAncestorContainer with class 'messagebody'
+        var commonAncestor = activeElement.commonAncestorContainer;
+        if (commonAncestor) {
+            var isMessageBody = false;
+            var parent = commonAncestor.parentNode;
+            while (parent) {
+                if (parent.className && parent.className.indexOf('messagebody') != -1) {
+                    isMessageBody = true;
+                    break;
                 }
+                parent = parent.parentNode;
+            }
+            if (!isMessageBody) {
+                return;
             }
+        }
 
+        if ( RT.Config.MessageBoxRichText ) {
+            selection = range_html(activeElement);
+        }
+        else {
             if (selection.toString)
                 selection = selection.toString();
 
-            if (typeof(selection) !== "string" || selection.length < 3)
-                return;
+            selection = selection.concat("\n\n");
+        }
+        if (typeof(selection) !== "string" || selection.length < 3)
+            return;
 
-            // TODO: wrap long lines before quoting
-            selection = selection.replace(/^/gm, "> ");
-            if ( RT.Config.MessageBoxRichText ) {
-                selection = selection.replace(/\r?\n/g, "<br>");
-                selection = selection.concat("<br><br>");
-            }
-            else {
-                selection = selection.concat("\n\n");
-            }
-            selection = encodeURIComponent(selection);
+        selection = encodeURIComponent(selection);
 
-            if ( !link.prop('data-href') ) {
-                link.prop('data-href', link.attr('href'));
-            }
-            link.attr("href", link.prop("data-href").concat("&UpdateContent=" + selection));
-        };
+        if ( !link.prop('data-href') ) {
+            link.prop('data-href', link.attr('href'));
+        }
+        link.attr("href", link.prop("data-href").concat("&QuoteContent=" + selection));
+    };
 
-        var apply_quote = function() {
-            var link = jQuery(this);
-            if (link.data("quote-selection"))
-                return;
-            link.data("quote-selection",true);
-            link.click(reply_from_selection);
-        };
-
-        jQuery(
-            ".reply-link, "         +
-            ".comment-link, "       +
-            "#page-actions-reply, " +
-            "#page-actions-comment"
-        ).each(apply_quote);
-
-        jQuery(document).ajaxComplete(function(ev){
-            jQuery(".reply-link, .comment-link").each(apply_quote);
-        });
-    }
+    var apply_quote = function() {
+        var link = jQuery(this);
+        if (link.data("quote-selection"))
+            return;
+        link.data("quote-selection",true);
+        link.click(reply_from_selection);
+    };
+
+    jQuery(
+        ".reply-link, "         +
+        ".comment-link, "       +
+        "#page-actions-reply, " +
+        "#page-actions-comment"
+    ).each(apply_quote);
+
+    jQuery(document).ajaxComplete(function(ev){
+        jQuery(".reply-link, .comment-link").each(apply_quote);
+    });
 });
diff --git a/t/api/txn_content.t b/t/api/txn_content.t
index 672d6c2e02..0327af82fd 100644
--- a/t/api/txn_content.t
+++ b/t/api/txn_content.t
@@ -1,7 +1,7 @@
 use warnings;
 use strict;
 
-use RT::Test tests => 4;
+use RT::Test tests => undef;
 use MIME::Entity;
 my $ticket = RT::Ticket->new(RT->SystemUser);
 my $mime   = MIME::Entity->build(
@@ -21,3 +21,59 @@ ok( $txn, 'got Create txn' );
 # for html. Our html -> text converter seems to add an extra trailing newline
 like( $txn->Content, qr/^\s*this is body\s*$/, "txn's html content converted to plain text" );
 is( $txn->Content(Type => 'text/html'), "this is body\n", "txn's html content" );
+
+
+# test RT::Transaction->QuoteContent
+{
+    {
+        my $got = RT::Transaction->QuoteContent(
+            Type        => 'text/plain',
+            Content     => 'foo',
+        );
+        is $got, "> foo", "ok";
+    }
+
+    {
+        my $got = RT::Transaction->QuoteContent(
+            Type        => 'text/html',
+            Content     => '<stron>jane & joe</strong>',
+        );
+        is $got, '<blockquote class="gmail_quote" type="cite">' . '<stron>jane & joe</strong>' . '</blockquote>', "ok",;
+    }
+
+    {
+        my $got = RT::Transaction->QuoteContent(
+            Type        => 'text/plain',
+            Content     => 'jane & joe',
+        );
+        is $got, "> jane & joe", "ok",;
+    }
+
+    {
+        my $got = RT::Transaction->QuoteContent(
+            Type        => 'text/html',
+            QuoteHeader => 'Nemo wrote:',
+            Content     => '<stron>jane & joe</strong>',
+        );
+        is $got,
+              '<div class="gmail_quote">Nemo wrote:<br />'
+            . '<blockquote class="gmail_quote" type="cite">'
+            . '<stron>jane & joe</strong>'
+            . '</blockquote>'
+            . '</div>',
+            "ok",
+            ;
+    }
+
+    {
+        my $got = RT::Transaction->QuoteContent(
+            Type        => 'text/plain',
+            QuoteHeader => 'Nemo wrote:',
+            Content     => 'foo',
+        );
+        is $got, "Nemo wrote:\n> foo", "ok";
+    }
+}
+
+
+done_testing;

-----------------------------------------------------------------------


hooks/post-receive
-- 
rt


More information about the rt-commit mailing list