Edgewall Software

Ticket #7145: 7145-ticket-preview-2-r10560.patch

File 7145-ticket-preview-2-r10560.patch, 17.7 KB (added by rblank, 15 months ago)

Same patch, but ticket comment preview is right below the comment editor.

  • trac/htdocs/css/ticket.css

    diff --git a/trac/htdocs/css/ticket.css b/trac/htdocs/css/ticket.css
    a b label[for=comment] { float: right } 
    136136form .field .wikitoolbar { margin-left: -1px } 
    137137form .field div.trac-resizable { width: 100% } 
    138138 
     139#propertyform { margin-bottom: 2em; } 
    139140#properties { white-space: nowrap; line-height: 160%; padding: .5em } 
    140141#properties table { border-spacing: 0; width: 100%; padding: 0 .5em } 
    141142#properties table th { 
  • trac/htdocs/js/auto_preview.js

    diff --git a/trac/htdocs/js/auto_preview.js b/trac/htdocs/js/auto_preview.js
    a b  
    1 // Automatic preview through XHR 
     1// Automatic form submission and preview through XHR 
    22 
    33(function($) { 
     4  // Enable automatic submission of forms. 
     5  // 
     6  // This method can be applied to a single form, where it enables 
     7  // auto-submission on all the editable elements that it contains. 
     8  // It can also be applied on a list of elements, in which case it 
     9  // enables auto-submission only for these elements. 
     10  // 
     11  // Arguments: 
     12  //  - `args`: additional form data to be passed with the XHR. 
     13  //  - `update`: the function that is called with the submission reply. It 
     14  //              is called with the request data and the reply. 
     15  $.fn.autoSubmit = function(args, update) { 
     16    if (this.length == 0 || auto_preview_timeout <= 0) 
     17      return this; 
     18    if (this[0].nodeName == 'FORM') { 
     19      var form = this; 
     20      var inputs = this.find("textarea, select, :text, :checkbox, :radio"); 
     21    } else { 
     22      var form = this.closest('form'); 
     23      var inputs = this; 
     24    } 
     25    var timeout = auto_preview_timeout * 1000; 
     26    var timer = null; 
     27    var updating = false; 
     28    var queued = false; 
     29     
     30    // Return true iff the values have changed 
     31    function values_changed(new_values) { 
     32      if (values.length != new_values.length) 
     33        return true; 
     34      for (var i in values) { 
     35        var value = values[i], new_value = new_values[i]; 
     36        if ((value.name != new_value.name) || (value.value != new_value.value)) 
     37          return true; 
     38      } 
     39      return false; 
     40    } 
     41     
     42    // Request a preview through XHR 
     43    function request() { 
     44      if (!updating) { 
     45        var new_values = form.serializeArray(); 
     46        if (values_changed(new_values)) { 
     47          values = new_values; 
     48          updating = true; 
     49           
     50          // Construct request data 
     51          var data = values.slice(0); 
     52          for (var key in args) 
     53            data.push({name: key, value: args[key]}); 
     54           
     55          $.ajax({ 
     56            type: form.attr('method'), url: form.attr('action'), 
     57            data: data, traditional: true, dataType: "html", 
     58            success: function(reply) { 
     59              if (queued) 
     60                timer = setTimeout(request, timeout); 
     61              updating = false; 
     62              queued = false; 
     63              update(data, reply); 
     64            }, 
     65            error: function(req, err, exc) { 
     66              updating = false; 
     67              queued = false; 
     68            }, 
     69          }); 
     70        } 
     71      } 
     72    } 
     73     
     74    // Trigger a request after the given timeout 
     75    function trigger() { 
     76      if (!updating) { 
     77        if (timer) 
     78          clearTimeout(timer); 
     79        timer = setTimeout(request, timeout); 
     80      } else { 
     81        queued = true; 
     82      } 
     83      return true; 
     84    } 
     85 
     86    var values = form.serializeArray(); 
     87    return inputs.each(function() { 
     88      $(this).keydown(trigger).keypress(trigger).change(trigger).blur(trigger); 
     89    }); 
     90  }; 
     91 
    492  // Enable automatic previewing to <textarea> elements. 
    593  // 
    694  // Arguments: 
     
    56144       
    57145      $(this).keydown(trigger).keypress(trigger).blur(request); 
    58146    }); 
    59   } 
     147  }; 
    60148})(jQuery); 
  • trac/ticket/templates/ticket.html

    diff --git a/trac/ticket/templates/ticket.html b/trac/ticket/templates/ticket.html
    a b  
    1919        $("div.description").find("h1,h2,h3,h4,h5,h6").addAnchor(_("Link to this section")); 
    2020        $(".foldable").enableFolding(false, true); 
    2121      <py:when test="ticket.exists"> 
    22         var args = {realm: "ticket", id: ${ticket.id}, escape_newlines: ${int(preserve_newlines)}} 
    23         $("#comment").autoPreview("${href.wiki_render()}", args, function(textarea, text, rendered) { 
    24             $("#ticketchange div.comment").html(rendered); 
    25             if (rendered) 
    26               $("#ticketchange").show(); 
    27             else if ($("#ticketchange ul.changes").length == 0) 
    28               $("#ticketchange").hide(); 
    29         }); 
    30         $("#trac-comment-editor textarea").autoPreview("${href.wiki_render()}", args, 
    31                                                        function(textarea, text, rendered) { 
    32           var comment = $("#trac-comment-editor").next("div.comment"); 
    33           comment.html(rendered); 
    34           if (rendered) 
    35             comment.show(); 
    36           else 
    37             comment.hide(); 
    38         }); 
    3922        $("#modify").parent().toggleClass("collapsed"); 
    4023        $(".trac-topnav a").click(function() { $("#modify").parent().removeClass("collapsed"); }); 
    4124 
     
    5033        } 
    5134        actions.click(updateActionFields); 
    5235        updateActionFields(); 
    53       </py:when> 
    54       <py:otherwise> 
    55         $("#field-summary").focus(); 
    56       </py:otherwise> 
    57       <py:if test="preview_mode"> 
     36         
     37        $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) { 
     38          var items = $(reply); 
     39          $("#ticket").replaceWith(items.filter('#ticket')); 
     40          var changes = $("#ticketchange-content").html(items.filter('ul.changes, div.comment')); 
     41          $("#ticketchange").toggle(changes.children().length != 0); 
     42        }); 
     43        $("#trac-comment-editor").autoSubmit({preview_comment: '1'}, function(data, reply) { 
     44          var comment = $("#trac-comment-editor").next("div.comment").html(reply); 
     45          comment.toggle(comment.children().length != 0); 
     46        }); 
     47        <py:if test="preview_mode"> 
    5848        $("#changelog").parent().toggleClass("collapsed"); 
    5949        $("#attachments").toggleClass("collapsed"); 
    6050        $("#trac-add-comment").scrollToTop(); 
    61       </py:if> 
     51        </py:if> 
     52      </py:when> 
     53      <py:otherwise> 
     54        $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) { 
     55          $('#ticket').replaceWith(reply); 
     56        }); 
     57        <py:if test="not preview_mode"> 
     58        $("#field-summary").focus(); 
     59        </py:if> 
     60      </py:otherwise> 
    6261      }); 
    6362    </script> 
    6463  </head> 
     
    7776                  has_edit_comment = 'TICKET_EDIT_COMMENT' in perm(ticket.resource); 
    7877                  has_property_editor = not version and version != 0 and not cnum_edit 
    7978                                        and (can_append or can_modify or can_edit or can_create)"> 
    80       <div class="trac-topnav" py:if="(ticket.exists or preview_mode) and has_property_editor"> 
     79      <div class="trac-topnav" py:if="ticket.exists and has_property_editor"> 
    8180        <a href="#propertyform" title="Go to the ticket editor">Modify</a> &darr; 
    8281      </div> 
    8382      <h1 id="trac-ticket-title" py:choose=""> 
     
    101100        </py:otherwise> 
    102101      </h1> 
    103102 
    104       <xi:include href="ticket_box.html" py:if="ticket.exists or preview_mode"/> 
    105103 
    106104      <py:if test="ticket.exists"> 
     105        <xi:include href="ticket_box.html" py:with="preview_mode = change_preview.fields"/> 
     106         
    107107        <!--! do not show attachments for old versions of this ticket or for new tickets --> 
    108108        <py:if test="not version and version != 0 and ticket.exists"> 
    109109          <xi:include href="list_of_attachments.html" 
     
    127127                             show_editor = can_edit_comment and str(change.cnum) == cnum_edit; 
    128128                             show_history = str(change.cnum) == cnum_hist; 
    129129                             max_version = max(change.comment_history); 
    130                              comment_version = (max_version, int(cversion or 0))[show_history]"> 
    131               <div class="change" id="${'cnum' in change and 'trac-change-%d' % change.cnum or None}"> 
     130                             comment_version = (max_version, int(cversion or 0))[show_history]; 
     131                             show_buttons = not show_editor and comment_version == max_version"> 
     132              <div class="change${not show_buttons and ' trac-nobuttons' or ''}" 
     133                   id="${'cnum' in change and 'trac-change-%d' % change.cnum or None}"> 
    132134                <h3 class="change"> 
    133135                  <span class="threading" py:if="'cnum' in change" 
    134136                        py:with="change_replies = replies.get(str(change.cnum), [])"> 
     
    150152                  </span> 
    151153                  <i18n:msg params="date, author">Changed ${dateinfo(change.date)} ago by ${authorinfo(change.author)}</i18n:msg> 
    152154                </h3> 
    153                 <py:if test="not show_editor and comment_version == max_version"> 
     155                <py:if test="show_buttons"> 
    154156                  <form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${change.cnum}"> 
    155157                    <div class="inlinebuttons"> 
    156158                      <input type="hidden" name="cnum_edit" value="${change.cnum}"/> 
     
    197199      <!--! End of the section we don't show on initial new tickets --> 
    198200 
    199201      <form py:if="has_property_editor" method="post" id="propertyform" 
    200             action="${ticket.exists and href.ticket(ticket.id) + '#trac-add-comment' or href.newticket()}"> 
     202            action="${href.ticket(ticket.id) + '#trac-add-comment' if ticket.exists 
     203                      else href.newticket() + '#ticket'}"> 
    201204        <!--! Add comment --> 
    202         <div py:if="ticket.exists and can_append" class="field" 
    203              py:with="show_comment_preview = (change_preview.fields or change_preview.comment) and cnum_edit is None"> 
     205        <div py:if="ticket.exists and can_append" class="field"> 
    204206          <div class="trac-nav"> 
    205207            <a href="#content" title="View ticket fields and description">View</a> &uarr; 
    206208          </div> 
     
    221223            may have failed. 
    222224          </div> 
    223225          <!--! Preview of ticket changes --> 
    224           <div id="ticketchange" class="ticketdraft" style="${not show_comment_preview and 'display: none' or None}"> 
     226          <div id="ticketchange" class="ticketdraft" 
     227               style="${(not (change_preview.fields or change_preview.comment) 
     228                         or cnum_edit is not None) and 'display: none' or None}"> 
    225229            <h3 class="change" id="${'cnum' in change_preview and 'comment:%d' % change_preview.cnum or None}"> 
    226230              <span class="threading" py:if="'replyto' in change_preview"> 
    227231                in reply to: ${commentref('&uarr;&nbsp;', change_preview.replyto)} 
    228232              </span> 
    229233              <i18n:msg params="author">Changed by ${authorinfo(change_preview.author)}</i18n:msg> 
    230234            </h3> 
    231             <xi:include href="ticket_change.html" py:with="change = change_preview"/> 
     235            <div id="ticketchange-content"><xi:include href="ticket_change.html" py:with="change = change_preview"/></div> 
    232236          </div> 
    233237        </div> 
    234238 
     239 
    235240        <div> 
    236241          <h2 py:if="ticket.exists" class="foldable">Modify Ticket</h2> 
    237242          <div id="modify"> 
     
    409414 
    410415      </form> 
    411416 
     417      <xi:include href="ticket_box.html" py:if="not ticket.exists" py:with="preview_mode = True"/> 
     418       
    412419      <div id="help" i18n:msg=""> 
    413420        <strong>Note:</strong> See 
    414421        <a href="${href.wiki('TracTickets')}">TracTickets</a> for help on using 
  • trac/ticket/templates/ticket_change.html

    diff --git a/trac/ticket/templates/ticket_change.html b/trac/ticket/templates/ticket_change.html
    a b Arguments: 
    1313      xmlns:i18n="http://genshi.edgewall.org/i18n" 
    1414      py:with="show_editor = value_of('show_editor', False)" py:strip=""> 
    1515  <ul py:if="change.fields" class="changes"> 
    16     <li py:for="field_name, field in change.fields.items()"> 
     16    <li py:for="field_name, field in sorted(change.fields.iteritems(), key=lambda item: item[1].label.lower())"> 
    1717      <strong>${field.label}</strong> 
    1818      <py:choose> 
    1919        <py:when test="field_name == 'attachment'"><i18n:msg params="name"> 
    Arguments: 
    3535      </py:choose> 
    3636    </li> 
    3737  </ul> 
    38   <form py:if="show_editor" id="trac-comment-editor" method="post" action="#comment:${change.cnum}"> 
     38  <form py:if="show_editor" id="trac-comment-editor" method="post" 
     39        action="${href.ticket(ticket.id) + '#comment:%d' % change.cnum}"> 
    3940    <div> 
    4041      <textarea name="edited_comment" class="wikitext trac-resizable" rows="10" cols="78"> 
    4142${(edited_comment, change.comment)[edited_comment is None]}</textarea> 
    Arguments: 
    5556         class="comment searchable ticketdraft" style="${not text and 'display: none' or None}" xml:space="preserve"> 
    5657      ${wiki_to_html(context, text, escape_newlines=preserve_newlines)} 
    5758    </div> 
    58     <div py:otherwise="" py:choose="" class="comment searchable" xml:space="preserve"> 
    59       <py:when test="show_history">${wiki_to_html(context, change.comment_history[int(cversion)].comment, 
    60                                                   escape_newlines=preserve_newlines)}</py:when> 
    61       <py:otherwise>${wiki_to_html(context, change.comment, escape_newlines=preserve_newlines)}</py:otherwise> 
     59    <div py:when="show_history" class="comment searchable" xml:space="preserve"> 
     60      ${wiki_to_html(context, change.comment_history[int(cversion)].comment, escape_newlines=preserve_newlines)} 
     61    </div> 
     62    <div py:when="change.comment" class="comment searchable" xml:space="preserve"> 
     63      ${wiki_to_html(context, change.comment, escape_newlines=preserve_newlines)} 
    6264    </div> 
    6365  </py:choose> 
    6466</html> 
  • new file trac/ticket/templates/ticket_preview.html

    diff --git a/trac/ticket/templates/ticket_preview.html b/trac/ticket/templates/ticket_preview.html
    new file mode 100644
    - +  
     1<!--! 
     2Render a ticket box preview and a ticket change preview, to be sent as a reply 
     3to a ticket change auto-preview. 
     4--> 
     5<html xmlns="http://www.w3.org/1999/xhtml" 
     6      xmlns:py="http://genshi.edgewall.org/" 
     7      xmlns:xi="http://www.w3.org/2001/XInclude" 
     8      xmlns:i18n="http://genshi.edgewall.org/i18n" 
     9      py:strip=""> 
     10  <xi:include href="ticket_box.html" py:with="can_append = 'TICKET_APPEND' in perm(ticket.resource)"/> 
     11  <xi:include href="ticket_change.html" py:with="change = change_preview"/> 
     12</html> 
  • trac/ticket/web_ui.py

    diff --git a/trac/ticket/web_ui.py b/trac/ticket/web_ui.py
    a b from trac.ticket.api import TicketSystem 
    3636from trac.ticket.model import Milestone, Ticket, group_milestones 
    3737from trac.ticket.notification import TicketNotifyEmail 
    3838from trac.timeline.api import ITimelineEventProvider 
    39 from trac.util import as_bool, get_reporter_id 
     39from trac.util import as_bool, as_int, get_reporter_id 
    4040from trac.util.datefmt import format_datetime, from_utimestamp, \ 
    4141                              to_utimestamp, utc 
    4242from trac.util.text import exception_to_unicode, obfuscate_email_address, \ 
    class TicketModule(Component): 
    441441 
    442442        data['fields'] = fields 
    443443 
     444        if req.get_header('X-Requested-With') == 'XMLHttpRequest': 
     445            data['preview_mode'] = True 
     446            return 'ticket_box.html', data, None 
     447 
    444448        add_stylesheet(req, 'common/css/ticket.css') 
    445449        add_script(req, 'common/js/folding.js') 
    446450        Chrome(self.env).add_wiki_toolbars(req) 
     451        Chrome(self.env).add_auto_preview(req) 
    447452        return 'ticket.html', data, None 
    448453 
    449454    def _process_ticket_request(self, req): 
    450455        id = int(req.args.get('id')) 
    451         version = req.args.get('version', None) 
    452         if version is not None: 
    453             try: 
    454                 version = int(version) 
    455             except ValueError: 
    456                 version = None 
     456        version = as_int(req.args.get('version'), None) 
     457        xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' 
     458         
     459        if xhr and 'preview_comment' in req.args: 
     460            context = web_context(req, 'ticket', id, version) 
     461            escape_newlines = self.must_preserve_newlines 
     462            rendered = format_to_html(self.env, context, 
     463                                      req.args.get('edited_comment', ''), 
     464                                      escape_newlines=escape_newlines) 
     465            req.send(rendered.encode('utf-8')) 
    457466 
    458467        req.perm('ticket', id, version).require('TICKET_VIEW') 
    459468        ticket = Ticket(self.env, id, version=version) 
    class TicketModule(Component): 
    572581        self._insert_ticket_data(req, ticket, data, 
    573582                                 get_reporter_id(req, 'author'), field_changes) 
    574583 
     584        if xhr: 
     585            data['preview_mode'] = bool(data['change_preview']['fields']) 
     586            return 'ticket_preview.html', data, None 
     587 
    575588        mime = Mimeview(self.env) 
    576589        format = req.args.get('format') 
    577590        if format: 
  • trac/wiki/web_api.py

    diff --git a/trac/wiki/web_api.py b/trac/wiki/web_api.py
    a b  
    1313 
    1414from trac.core import * 
    1515from trac.resource import Resource 
     16from trac.util import as_int 
    1617from trac.web.api import IRequestHandler 
    1718from trac.web.chrome import web_context 
    1819from trac.wiki.formatter import format_to 
    class WikiRenderer(Component): 
    3637            req.perm.require('TRAC_ADMIN') 
    3738        realm = req.args.get('realm', 'wiki') 
    3839        id = req.args.get('id') 
    39         version = req.args.get('version') 
    40         if version is not None: 
    41             try: 
    42                 version = int(version) 
    43             except ValueError: 
    44                 version = None 
     40        version = as_int(req.args.get('version'), None) 
    4541        text = req.args.get('text', '') 
    4642        flavor = req.args.get('flavor') 
    4743        options = {}