Ticket #7145: 7145-ticket-preview-r10632.patch
| File 7145-ticket-preview-r10632.patch, 18.9 KB (added by rblank, 15 months ago) |
|---|
-
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 } 136 136 form .field .wikitoolbar { margin-left: -1px } 137 137 form .field div.trac-resizable { width: 100% } 138 138 139 #propertyform { margin-bottom: 2em; } 139 140 #properties { white-space: nowrap; line-height: 160%; padding: .5em } 140 141 #properties table { border-spacing: 0; width: 100%; padding: 0 .5em } 141 142 #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 XHR1 // Automatic form submission and preview through XHR 2 2 3 3 (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 4 92 // Enable automatic previewing to <textarea> elements. 5 93 // 6 94 // Arguments: … … 56 144 57 145 $(this).keydown(trigger).keypress(trigger).blur(request); 58 146 }); 59 } 147 }; 60 148 })(jQuery); -
trac/ticket/templates/ticket.html
diff --git a/trac/ticket/templates/ticket.html b/trac/ticket/templates/ticket.html
a b 18 18 jQuery(document).ready(function($) { 19 19 $("div.description").find("h1,h2,h3,h4,h5,h6").addAnchor(_("Link to this section")); 20 20 $(".foldable").enableFolding(false, true); 21 <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 }); 21 <py:when test="ticket.exists">/*<![CDATA[*/ 39 22 $("#modify").parent().toggleClass("collapsed"); 40 23 $(".trac-topnav a").click(function() { $("#modify").parent().removeClass("collapsed"); }); 41 24 … … 50 33 } 51 34 actions.click(updateActionFields); 52 35 updateActionFields(); 53 </py:when> 54 <py:otherwise> 55 $("#field-summary").focus(); 56 </py:otherwise> 57 <py:if test="preview_mode"> 36 37 var comment_focused = false; 38 $("#comment").focus(function() { comment_focused = true; console.log("focus"); }) 39 .blur(function() { comment_focused = false; console.log("blur"); }); 40 $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) { 41 var items = $(reply); 42 $("#ticket").replaceWith(items.filter('#ticket')); 43 var changes = $("#ticketchange-content").html(items.filter('ul.changes, div.comment')); 44 var show_preview = changes.children().length != 0; 45 $("#ticketchange").toggle(show_preview); 46 if (show_preview && comment_focused) 47 $("#modify").parent().addClass("collapsed"); 48 }); 49 $("#trac-comment-editor").autoSubmit({preview_comment: '1'}, function(data, reply) { 50 var comment = $("#trac-comment-editor").next("div.comment").html(reply); 51 comment.toggle(comment.children().length != 0); 52 }); 53 /*]]>*/ 54 <py:if test="preview_mode"> 58 55 $("#changelog").parent().toggleClass("collapsed"); 59 56 $("#attachments").toggleClass("collapsed"); 60 57 $("#trac-add-comment").scrollToTop(); 61 </py:if> 58 </py:if> 59 </py:when> 60 <py:otherwise> 61 $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) { 62 $('#ticket').replaceWith(reply); 63 }); 64 <py:if test="not preview_mode"> 65 $("#field-summary").focus(); 66 </py:if> 67 </py:otherwise> 62 68 }); 63 69 </script> 64 70 </head> … … 77 83 has_edit_comment = 'TICKET_EDIT_COMMENT' in perm(ticket.resource); 78 84 has_property_editor = not version and version != 0 and not cnum_edit 79 85 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">86 <div class="trac-topnav" py:if="ticket.exists and has_property_editor"> 81 87 <a href="#propertyform" title="Go to the ticket editor">Modify</a> ↓ 82 88 </div> 83 89 <h1 id="trac-ticket-title" py:choose=""> … … 101 107 </py:otherwise> 102 108 </h1> 103 109 104 <xi:include href="ticket_box.html" py:if="ticket.exists or preview_mode"/>105 110 106 111 <py:if test="ticket.exists"> 112 <xi:include href="ticket_box.html" py:with="preview_mode = change_preview.fields"/> 113 107 114 <!--! do not show attachments for old versions of this ticket or for new tickets --> 108 115 <py:if test="not version and version != 0 and ticket.exists"> 109 116 <xi:include href="list_of_attachments.html" … … 127 134 show_editor = can_edit_comment and str(change.cnum) == cnum_edit; 128 135 show_history = str(change.cnum) == cnum_hist; 129 136 max_version = max(change.comment_history); 130 comment_version = int(cversion or 0) if show_history else max_version"> 131 <div class="change" id="${'trac-change-%d' % change.cnum if 'cnum' in change else None}"> 137 comment_version = int(cversion or 0) if show_history else max_version; 138 show_buttons = not show_editor and comment_version == max_version"> 139 <div class="change${not show_buttons and ' trac-nobuttons' or ''}" 140 id="${'trac-change-%d' % change.cnum if 'cnum' in change else None}"> 132 141 <h3 class="change"> 133 142 <span class="threading" py:if="'cnum' in change" 134 143 py:with="change_replies = replies.get(str(change.cnum), [])"> … … 150 159 </span> 151 160 <i18n:msg params="date, author">Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)}</i18n:msg> 152 161 </h3> 153 <py:if test=" not show_editor and comment_version == max_version">162 <py:if test="show_buttons"> 154 163 <form py:if="'cnum' in change and can_edit_comment" method="get" action="#comment:${change.cnum}"> 155 164 <div class="inlinebuttons"> 156 165 <input type="hidden" name="cnum_edit" value="${change.cnum}"/> … … 197 206 <!--! End of the section we don't show on initial new tickets --> 198 207 199 208 <form py:if="has_property_editor" method="post" id="propertyform" 200 action="${href.ticket(ticket.id) + '#trac-add-comment' if ticket.exists else href.newticket()}"> 209 action="${href.ticket(ticket.id) + '#trac-add-comment' if ticket.exists 210 else href.newticket() + '#ticket'}"> 201 211 <!--! 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"> 212 <div py:if="ticket.exists and can_append" class="field"> 204 213 <div class="trac-nav"> 205 214 <a href="#content" title="View ticket fields and description">View</a> ↑ 206 215 </div> … … 220 229 Warnings are shown at the <a href="#warning">top of the page</a>. The ticket validation 221 230 may have failed. 222 231 </div> 223 <!--! Preview of ticket changes -->224 <div id="ticketchange" class="ticketdraft" style="${'display: none' if not show_comment_preview else None}">225 <h3 class="change" id="${'comment:%d' % change_preview.cnum if 'cnum' in change_preview else None}">226 <span class="threading" py:if="'replyto' in change_preview">227 in reply to: ${commentref('↑ ', change_preview.replyto)}228 </span>229 <i18n:msg params="author">Changed by ${authorinfo(change_preview.author)}</i18n:msg>230 </h3>231 <xi:include href="ticket_change.html" py:with="change = change_preview"/>232 </div>233 232 </div> 234 233 235 234 <div> … … 386 385 </fieldset> 387 386 </div> 388 387 388 <!--! Preview of ticket changes --> 389 <div py:if="ticket.exists and can_append" id="ticketchange" class="ticketdraft" 390 style="${'display: none' if not (change_preview.fields or change_preview.comment) 391 or cnum_edit is not None else None}"> 392 <h3 class="change" id="${'comment:%d' % change_preview.cnum if 'cnum' in change_preview else None}"> 393 <span class="threading" py:if="'replyto' in change_preview"> 394 in reply to: ${commentref('↑ ', change_preview.replyto)} 395 </span> 396 <i18n:msg params="author">Changed by ${authorinfo(change_preview.author)}</i18n:msg> 397 </h3> 398 <div id="ticketchange-content"><xi:include href="ticket_change.html" py:with="change = change_preview"/></div> 399 </div> 400 389 401 <!--! Attachment on creation checkbox --> 390 402 <p py:if="not ticket.exists and 'ATTACHMENT_CREATE' in perm(ticket.resource.child('attachment'))"> 391 403 <label> … … 409 421 410 422 </form> 411 423 424 <xi:include href="ticket_box.html" py:if="not ticket.exists" py:with="preview_mode = True"/> 425 412 426 <div id="help" i18n:msg=""> 413 427 <strong>Note:</strong> See 414 428 <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: 13 13 xmlns:i18n="http://genshi.edgewall.org/i18n" 14 14 py:with="show_editor = value_of('show_editor', False)" py:strip=""> 15 15 <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())"> 17 17 <strong>${field.label}</strong> 18 18 <py:choose> 19 19 <py:when test="field_name == 'attachment'"><i18n:msg params="name"> … … Arguments: 35 35 </py:choose> 36 36 </li> 37 37 </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}"> 39 40 <div> 40 41 <textarea name="edited_comment" class="wikitext trac-resizable" rows="10" cols="78"> 41 42 ${edited_comment if edited_comment is not None else change.comment}</textarea> … … Arguments: 56 57 class="comment searchable ticketdraft" style="${'display: none' if not text else None}" xml:space="preserve"> 57 58 ${wiki_to_html(context, text, escape_newlines=preserve_newlines)} 58 59 </div> 59 <div py:otherwise="" py:choose="" class="comment searchable" xml:space="preserve"> 60 <py:when test="show_history">${wiki_to_html(context, change.comment_history[int(cversion)].comment, 61 escape_newlines=preserve_newlines)}</py:when> 62 <py:otherwise>${wiki_to_html(context, change.comment, escape_newlines=preserve_newlines)}</py:otherwise> 60 <div py:when="show_history" class="comment searchable" xml:space="preserve"> 61 ${wiki_to_html(context, change.comment_history[int(cversion)].comment, escape_newlines=preserve_newlines)} 62 </div> 63 <div py:when="change.comment" class="comment searchable" xml:space="preserve"> 64 ${wiki_to_html(context, change.comment, escape_newlines=preserve_newlines)} 63 65 </div> 64 66 </py:choose> 65 67 </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 <!--! 2 Render a ticket box preview and a ticket change preview, to be sent as a reply 3 to 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 36 36 from trac.ticket.model import Milestone, Ticket, group_milestones 37 37 from trac.ticket.notification import TicketNotifyEmail 38 38 from trac.timeline.api import ITimelineEventProvider 39 from trac.util import as_bool, get_reporter_id39 from trac.util import as_bool, as_int, get_reporter_id 40 40 from trac.util.datefmt import format_datetime, from_utimestamp, \ 41 41 to_utimestamp, utc 42 42 from trac.util.text import exception_to_unicode, obfuscate_email_address, \ … … class TicketModule(Component): 441 441 442 442 data['fields'] = fields 443 443 444 if req.get_header('X-Requested-With') == 'XMLHttpRequest': 445 data['preview_mode'] = True 446 return 'ticket_box.html', data, None 447 444 448 add_stylesheet(req, 'common/css/ticket.css') 445 449 add_script(req, 'common/js/folding.js') 446 450 Chrome(self.env).add_wiki_toolbars(req) 451 Chrome(self.env).add_auto_preview(req) 447 452 return 'ticket.html', data, None 448 453 449 454 def _process_ticket_request(self, req): 450 455 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')) 457 466 458 467 req.perm('ticket', id, version).require('TICKET_VIEW') 459 468 ticket = Ticket(self.env, id, version=version) … … class TicketModule(Component): 572 581 self._insert_ticket_data(req, ticket, data, 573 582 get_reporter_id(req, 'author'), field_changes) 574 583 584 if xhr: 585 data['preview_mode'] = bool(data['change_preview']['fields']) 586 return 'ticket_preview.html', data, None 587 575 588 mime = Mimeview(self.env) 576 589 format = req.args.get('format') 577 590 if format: -
trac/wiki/web_api.py
diff --git a/trac/wiki/web_api.py b/trac/wiki/web_api.py
a b 13 13 14 14 from trac.core import * 15 15 from trac.resource import Resource 16 from trac.util import as_int 16 17 from trac.web.api import IRequestHandler 17 18 from trac.web.chrome import web_context 18 19 from trac.wiki.formatter import format_to … … class WikiRenderer(Component): 36 37 req.perm.require('TRAC_ADMIN') 37 38 realm = req.args.get('realm', 'wiki') 38 39 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) 45 41 text = req.args.get('text', '') 46 42 flavor = req.args.get('flavor') 47 43 options = {}
