5 * sphinx.websupport utilties for all documentation.
7 * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS.
8 * :license: BSD, see LICENSE for details.
13 $
.fn
.autogrow
= function() {
14 return this.each(function() {
17 $
.fn
.autogrow
.resize(textarea
);
21 textarea
.interval
= setInterval(function() {
22 $
.fn
.autogrow
.resize(textarea
);
26 clearInterval(textarea
.interval
);
31 $
.fn
.autogrow
.resize
= function(textarea
) {
32 var lineHeight
= parseInt($
(textarea
).css('line-height'), 10);
33 var lines
= textarea
.value
.split('\n');
34 var columns
= textarea
.cols
;
36 $
.each(lines
, function() {
37 lineCount
+= Math
.ceil(this.length
/ columns
) ||
1;
39 var height
= lineHeight
* (lineCount
+ 1);
40 $
(textarea
).css('height', height
);
52 function initEvents() {
53 $
('a.comment-close').live("click", function(event
) {
54 event
.preventDefault();
55 hide($
(this).attr('id').substring(2));
57 $
('a.vote').live("click", function(event
) {
58 event
.preventDefault();
61 $
('a.reply').live("click", function(event
) {
62 event
.preventDefault();
63 openReply($
(this).attr('id').substring(2));
65 $
('a.close-reply').live("click", function(event
) {
66 event
.preventDefault();
67 closeReply($
(this).attr('id').substring(2));
69 $
('a.sort-option').live("click", function(event
) {
70 event
.preventDefault();
71 handleReSort($
(this));
73 $
('a.show-proposal').live("click", function(event
) {
74 event
.preventDefault();
75 showProposal($
(this).attr('id').substring(2));
77 $
('a.hide-proposal').live("click", function(event
) {
78 event
.preventDefault();
79 hideProposal($
(this).attr('id').substring(2));
81 $
('a.show-propose-change').live("click", function(event
) {
82 event
.preventDefault();
83 showProposeChange($
(this).attr('id').substring(2));
85 $
('a.hide-propose-change').live("click", function(event
) {
86 event
.preventDefault();
87 hideProposeChange($
(this).attr('id').substring(2));
89 $
('a.accept-comment').live("click", function(event
) {
90 event
.preventDefault();
91 acceptComment($
(this).attr('id').substring(2));
93 $
('a.delete-comment').live("click", function(event
) {
94 event
.preventDefault();
95 deleteComment($
(this).attr('id').substring(2));
97 $
('a.comment-markup').live("click", function(event
) {
98 event
.preventDefault();
99 toggleCommentMarkupBox($
(this).attr('id').substring(2));
104 * Set comp, which is a comparator function used for sorting and
105 * inserting comments into the list.
107 function setComparator() {
108 // If the first three letters are "asc", sort in ascending order
109 // and remove the prefix.
110 if (by
.substring(0,3) == 'asc') {
111 var i
= by
.substring(3);
112 comp
= function(a
, b
) { return a
[i] - b
[i]; };
114 // Otherwise sort in descending order.
115 comp
= function(a
, b
) { return b
[by] - a
[by]; };
118 // Reset link styles and format the selected sort option.
119 $
('a.sel').attr('href', '#').removeClass('sel');
120 $
('a.by' + by
).removeAttr('href').addClass('sel');
124 * Create a comp function. If the user has preferences stored in
125 * the sortBy cookie, use those, otherwise use the default.
127 function initComparator() {
128 by
= 'rating'; // Default to sort by rating.
129 // If the sortBy cookie is set, use that instead.
130 if (document
.cookie
.length
> 0) {
131 var start
= document
.cookie
.indexOf('sortBy=');
134 var end
= document
.cookie
.indexOf(";", start
);
136 end
= document
.cookie
.length
;
137 by
= unescape(document
.cookie
.substring(start
, end
));
145 * Show a comment div.
148 $
('#ao' + id
).hide();
149 $
('#ah' + id
).show();
150 var context
= $
.extend({id
: id
}, opts
);
151 var popup
= $
(renderTemplate(popupTemplate
, context
)).hide();
152 popup
.find('textarea[name="proposal"]').hide();
153 popup
.find('a.by' + by
).addClass('sel');
154 var form
= popup
.find('#cf' + id
);
155 form
.submit(function(event
) {
156 event
.preventDefault();
159 $
('#s' + id
).after(popup
);
160 popup
.slideDown('fast', function() {
166 * Hide a comment div.
169 $
('#ah' + id
).hide();
170 $
('#ao' + id
).show();
171 var div
= $
('#sc' + id
);
172 div
.slideUp('fast', function() {
178 * Perform an ajax request to get comments for a node
179 * and insert the comments into the comments tree.
181 function getComments(id
) {
184 url
: opts
.getCommentsURL
,
186 success
: function(data
, textStatus
, request
) {
187 var ul
= $
('#cl' + id
);
190 .find('textarea[name="proposal"]')
191 .data('source', data
.source
);
193 if (data
.comments
.length
=== 0) {
194 ul
.html('<li>No comments yet.</li>');
195 ul
.data('empty', true);
197 // If there are comments, sort them and put them in the list.
198 var comments
= sortComments(data
.comments
);
199 speed
= data
.comments
.length
* 100;
200 appendComments(comments
, ul
);
201 ul
.data('empty', false);
203 $
('#cn' + id
).slideUp(speed
+ 200);
206 error
: function(request
, textStatus
, error
) {
207 showError('Oops, there was a problem retrieving the comments.');
214 * Add a comment via ajax and insert the comment into the comment tree.
216 function addComment(form
) {
217 var node_id
= form
.find('input[name="node"]').val();
218 var parent_id
= form
.find('input[name="parent"]').val();
219 var text
= form
.find('textarea[name="comment"]').val();
220 var proposal
= form
.find('textarea[name="proposal"]').val();
223 showError('Please enter a comment.');
227 // Disable the form that is being submitted.
228 form
.find('textarea,input').attr('disabled', 'disabled');
230 // Send the comment to the server.
233 url
: opts
.addCommentURL
,
241 success
: function(data
, textStatus
, error
) {
244 hideProposeChange(node_id
);
246 form
.find('textarea')
248 .add(form
.find('input'))
249 .removeAttr('disabled');
250 var ul
= $
('#cl' + (node_id || parent_id
));
251 if (ul
.data('empty')) {
253 ul
.data('empty', false);
255 insertComment(data
.comment
);
256 var ao
= $
('#ao' + node_id
);
257 ao
.find('img').attr({'src': opts
.commentBrightImage
});
259 // if this was a "root" comment, remove the commenting box
260 // (the user can get it back by reopening the comment popup)
261 $
('#ca' + node_id
).slideUp();
264 error
: function(request
, textStatus
, error
) {
265 form
.find('textarea,input').removeAttr('disabled');
266 showError('Oops, there was a problem adding the comment.');
272 * Recursively append comments to the main comment list and children
273 * lists, creating the comment tree.
275 function appendComments(comments
, ul
) {
276 $
.each(comments
, function() {
277 var div
= createCommentDiv(this);
278 ul
.append($
(document
.createElement('li')).html(div
));
279 appendComments(this.children
, div
.find('ul.comment-children'));
280 // To avoid stagnating data, don't store the comments children in data.
281 this.children
= null;
282 div
.data('comment', this);
287 * After adding a new comment, it must be inserted in the correct
288 * location in the comment tree.
290 function insertComment(comment
) {
291 var div
= createCommentDiv(comment
);
293 // To avoid stagnating data, don't store the comments children in data.
294 comment
.children
= null;
295 div
.data('comment', comment
);
297 var ul
= $
('#cl' + (comment
.node || comment
.parent
));
298 var siblings
= getChildren(ul
);
300 var li
= $
(document
.createElement('li'));
303 // Determine where in the parents children list to insert this comment.
304 for(i
=0; i
< siblings
.length
; i
++) {
305 if (comp(comment
, siblings
[i]) <= 0) {
306 $
('#cd' + siblings
[i].id
)
308 .before(li
.html(div
));
309 li
.slideDown('fast');
314 // If we get here, this comment rates lower than all the others,
315 // or it is the only comment in the list.
316 ul
.append(li
.html(div
));
317 li
.slideDown('fast');
320 function acceptComment(id
) {
323 url
: opts
.acceptCommentURL
,
325 success
: function(data
, textStatus
, request
) {
326 $
('#cm' + id
).fadeOut('fast');
327 $
('#cd' + id
).removeClass('moderate');
329 error
: function(request
, textStatus
, error
) {
330 showError('Oops, there was a problem accepting the comment.');
335 function deleteComment(id
) {
338 url
: opts
.deleteCommentURL
,
340 success
: function(data
, textStatus
, request
) {
341 var div
= $
('#cd' + id
);
342 if (data
== 'delete') {
343 // Moderator mode: remove the comment and all children immediately
344 div
.slideUp('fast', function() {
349 // User mode: only mark the comment as deleted
351 .find('span.user-id:first')
352 .text('[deleted]').end()
353 .find('div.comment-text:first')
354 .text('[deleted]').end()
355 .find('#cm' + id
+ ', #dc' + id
+ ', #ac' + id
+ ', #rc' + id
+
356 ', #sp' + id
+ ', #hp' + id
+ ', #cr' + id
+ ', #rl' + id
)
358 var comment
= div
.data('comment');
359 comment
.username
= '[deleted]';
360 comment
.text
= '[deleted]';
361 div
.data('comment', comment
);
363 error
: function(request
, textStatus
, error
) {
364 showError('Oops, there was a problem deleting the comment.');
369 function showProposal(id
) {
370 $
('#sp' + id
).hide();
371 $
('#hp' + id
).show();
372 $
('#pr' + id
).slideDown('fast');
375 function hideProposal(id
) {
376 $
('#hp' + id
).hide();
377 $
('#sp' + id
).show();
378 $
('#pr' + id
).slideUp('fast');
381 function showProposeChange(id
) {
382 $
('#pc' + id
).hide();
383 $
('#hc' + id
).show();
384 var textarea
= $
('#pt' + id
);
385 textarea
.val(textarea
.data('source'));
386 $
.fn
.autogrow
.resize(textarea
[0]);
387 textarea
.slideDown('fast');
390 function hideProposeChange(id
) {
391 $
('#hc' + id
).hide();
392 $
('#pc' + id
).show();
393 var textarea
= $
('#pt' + id
);
394 textarea
.val('').removeAttr('disabled');
395 textarea
.slideUp('fast');
398 function toggleCommentMarkupBox(id
) {
399 $
('#mb' + id
).toggle();
402 /** Handle when the user clicks on a sort by link. */
403 function handleReSort(link
) {
404 var classes
= link
.attr('class').split(/\s
+/);
405 for (var i
=0; i
<classes
.length
; i
++) {
406 if (classes
[i] != 'sort-option') {
407 by
= classes
[i].substring(2);
411 // Save/update the sortBy cookie.
412 var expiration
= new Date();
413 expiration
.setDate(expiration
.getDate() + 365);
414 document
.cookie
= 'sortBy=' + escape(by
) +
415 ';expires=' + expiration
.toUTCString();
416 $
('ul.comment-ul').each(function(index
, ul
) {
417 var comments
= getChildren($
(ul
), true);
418 comments
= sortComments(comments
);
419 appendComments(comments
, $
(ul
).empty());
424 * Function to process a vote when a user clicks an arrow.
426 function handleVote(link
) {
428 showError("You'll need to login to vote.");
432 var id
= link
.attr('id');
434 // Didn't click on one of the voting arrows.
437 // If it is an unvote, the new vote value is 0,
438 // Otherwise it's 1 for an upvote, or -1 for a downvote.
440 if (id
.charAt(1) != 'u') {
441 value
= id
.charAt(0) == 'u' ?
1 : -1;
443 // The data to be sent to the server.
445 comment_id
: id
.substring(2),
449 // Swap the vote and unvote links.
451 $
('#' + id
.charAt(0) + (id
.charAt(1) == 'u' ?
'v' : 'u') + d
.comment_id
)
454 // The div the comment is displayed in.
455 var div
= $
('div#cd' + d
.comment_id
);
456 var data
= div
.data('comment');
458 // If this is not an unvote, and the other vote arrow has
459 // already been pressed, unpress it.
460 if ((d
.value
!== 0) && (data
.vote
=== d
.value
* -1)) {
461 $
('#' + (d
.value
== 1 ?
'd' : 'u') + 'u' + d
.comment_id
).hide();
462 $
('#' + (d
.value
== 1 ?
'd' : 'u') + 'v' + d
.comment_id
).show();
465 // Update the comments rating in the local data.
466 data
.rating
+= (data
.vote
=== 0) ? d
.value
: (d
.value
- data
.vote
);
468 div
.data('comment', data
);
470 // Change the rating text.
471 div
.find('.rating:first')
472 .text(data
.rating
+ ' point' + (data
.rating
== 1 ?
'' : 's'));
474 // Send the vote information to the server.
477 url
: opts
.processVoteURL
,
479 error
: function(request
, textStatus
, error
) {
480 showError('Oops, there was a problem casting that vote.');
486 * Open a reply form used to reply to an existing comment.
488 function openReply(id
) {
489 // Swap out the reply link for the hide link
490 $
('#rl' + id
).hide();
491 $
('#cr' + id
).show();
493 // Add the reply li to the children ul.
494 var div
= $
(renderTemplate(replyTemplate
, {id
: id
})).hide();
497 // Setup the submit handler for the reply form.
499 .submit(function(event
) {
500 event
.preventDefault();
501 addComment($
('#rf' + id
));
504 .find('input[type=button]')
508 div
.slideDown('fast', function() {
509 $
('#rf' + id
).find('textarea').focus();
514 * Close the reply form opened with openReply.
516 function closeReply(id
) {
517 // Remove the reply div from the DOM.
518 $
('#rd' + id
).slideUp('fast', function() {
522 // Swap out the hide link for the reply link
523 $
('#cr' + id
).hide();
524 $
('#rl' + id
).show();
528 * Recursively sort a tree of comments using the comp comparator.
530 function sortComments(comments
) {
532 $
.each(comments
, function() {
533 this.children
= sortComments(this.children
);
539 * Get the children comments from a ul. If recursive is true,
540 * recursively include childrens' children.
542 function getChildren(ul
, recursive
) {
544 ul
.children().children("[id^='cd']")
546 var comment
= $
(this).data('comment');
548 comment
.children
= getChildren($
(this).find('#cl' + comment
.id
), true);
549 children
.push(comment
);
554 /** Create a div to display a comment in. */
555 function createCommentDiv(comment
) {
556 if (!comment
.displayed
&& !opts
.moderator
) {
557 return $
('<div class="moderate">Thank you! Your comment will show up '
558 + 'once it is has been approved by a moderator.</div>');
560 // Prettify the comment rating.
561 comment
.pretty_rating
= comment
.rating
+ ' point' +
562 (comment
.rating
== 1 ?
'' : 's');
563 // Make a class (for displaying not yet moderated comments differently)
564 comment
.css_class
= comment
.displayed ?
'' : ' moderate';
565 // Create a div for this comment.
566 var context
= $
.extend({}, opts
, comment
);
567 var div
= $
(renderTemplate(commentTemplate
, context
));
569 // If the user has voted on this comment, highlight the correct arrow.
571 var direction
= (comment
.vote
== 1) ?
'u' : 'd';
572 div
.find('#' + direction
+ 'v' + comment
.id
).hide();
573 div
.find('#' + direction
+ 'u' + comment
.id
).show();
576 if (opts
.moderator || comment
.text
!= '[deleted]') {
577 div
.find('a.reply').show();
578 if (comment
.proposal_diff
)
579 div
.find('#sp' + comment
.id
).show();
580 if (opts
.moderator
&& !comment
.displayed
)
581 div
.find('#cm' + comment
.id
).show();
582 if (opts
.moderator ||
(opts
.username
== comment
.username
))
583 div
.find('#dc' + comment
.id
).show();
589 * A simple template renderer. Placeholders such as <%id%> are replaced
590 * by context['id'] with items being escaped. Placeholders such as <#id#>
593 function renderTemplate(template
, context
) {
594 var esc
= $
(document
.createElement('div'));
596 function handle(ph
, escape
) {
598 $
.each(ph
.split('.'), function() {
601 return escape ? esc
.text(cur ||
"").html() : cur
;
604 return template
.replace(/<([%#
])([\w\
.]*)\
1>/g
, function() {
605 return handle(arguments
[2], arguments
[1] == '%' ?
true : false);
609 /** Flash an error message briefly. */
610 function showError(message
) {
611 $
(document
.createElement('div')).attr({'class': 'popup-error'})
612 .append($
(document
.createElement('div'))
613 .attr({'class': 'error-message'}).text(message
))
620 /** Add a link the user uses to open the comments popup. */
621 $
.fn
.comment
= function() {
622 return this.each(function() {
623 var id
= $
(this).attr('id').substring(1);
624 var count
= COMMENT_METADATA
[id];
625 var title
= count
+ ' comment' + (count
== 1 ?
'' : 's');
626 var image
= count
> 0 ? opts
.commentBrightImage
: opts
.commentImage
;
627 var addcls
= count
== 0 ?
' nocomment' : '';
630 $
(document
.createElement('a')).attr({
632 'class': 'sphinx-comment-open' + addcls
,
635 .append($
(document
.createElement('img')).attr({
640 .click(function(event
) {
641 event
.preventDefault();
642 show($
(this).attr('id').substring(2));
646 $
(document
.createElement('a')).attr({
648 'class': 'sphinx-comment-close hidden',
651 .append($
(document
.createElement('img')).attr({
652 src
: opts
.closeCommentImage
,
656 .click(function(event
) {
657 event
.preventDefault();
658 hide($
(this).attr('id').substring(2));
665 processVoteURL
: '/_process_vote',
666 addCommentURL
: '/_add_comment',
667 getCommentsURL
: '/_get_comments',
668 acceptCommentURL
: '/_accept_comment',
669 deleteCommentURL
: '/_delete_comment',
670 commentImage
: '/static/_static/comment.png',
671 closeCommentImage
: '/static/_static/comment-close.png',
672 loadingImage
: '/static/_static/ajax-loader.gif',
673 commentBrightImage
: '/static/_static/comment-bright.png',
674 upArrow
: '/static/_static/up.png',
675 downArrow
: '/static/_static/down.png',
676 upArrowPressed
: '/static/_static/up-pressed.png',
677 downArrowPressed
: '/static/_static/down-pressed.png',
682 if (typeof COMMENT_OPTIONS
!= "undefined") {
683 opts
= jQuery
.extend(opts
, COMMENT_OPTIONS
);
686 var popupTemplate
= '\
687 <div class="sphinx-comments" id="sc<%id%>">\
688 <p class="sort-options">\
690 <a href="#" class="sort-option byrating">best rated</a>\
691 <a href="#" class="sort-option byascage">newest</a>\
692 <a href="#" class="sort-option byage">oldest</a>\
694 <div class="comment-header">Comments</div>\
695 <div class="comment-loading" id="cn<%id%>">\
696 loading comments... <img src="<%loadingImage%>" alt="" /></div>\
697 <ul id="cl<%id%>" class="comment-ul"></ul>\
699 <p class="add-a-comment">Add a comment\
700 (<a href="#" class="comment-markup" id="ab<%id%>">markup</a>):</p>\
701 <div class="comment-markup-box" id="mb<%id%>">\
702 reStructured text markup: <i>*emph*</i>, <b>**strong**</b>, \
704 code blocks: <tt>::</tt> and an indented block after blank line</div>\
705 <form method="post" id="cf<%id%>" class="comment-form" action="">\
706 <textarea name="comment" cols="80"></textarea>\
707 <p class="propose-button">\
708 <a href="#" id="pc<%id%>" class="show-propose-change">\
709 Propose a change ▹\
711 <a href="#" id="hc<%id%>" class="hide-propose-change">\
712 Propose a change ▿\
715 <textarea name="proposal" id="pt<%id%>" cols="80"\
716 spellcheck="false"></textarea>\
717 <input type="submit" value="Add comment" />\
718 <input type="hidden" name="node" value="<%id%>" />\
719 <input type="hidden" name="parent" value="" />\
724 var commentTemplate
= '\
725 <div id="cd<%id%>" class="sphinx-comment<%css_class%>">\
728 <a href="#" id="uv<%id%>" class="vote" title="vote up">\
729 <img src="<%upArrow%>" />\
731 <a href="#" id="uu<%id%>" class="un vote" title="vote up">\
732 <img src="<%upArrowPressed%>" />\
736 <a href="#" id="dv<%id%>" class="vote" title="vote down">\
737 <img src="<%downArrow%>" id="da<%id%>" />\
739 <a href="#" id="du<%id%>" class="un vote" title="vote down">\
740 <img src="<%downArrowPressed%>" />\
744 <div class="comment-content">\
745 <p class="tagline comment">\
746 <span class="user-id"><%username%></span>\
747 <span class="rating"><%pretty_rating%></span>\
748 <span class="delta"><%time.delta%></span>\
750 <div class="comment-text comment"><#text#></div>\
751 <p class="comment-opts comment">\
752 <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a>\
753 <a href="#" class="close-reply" id="cr<%id%>">reply ▿</a>\
754 <a href="#" id="sp<%id%>" class="show-proposal">proposal ▹</a>\
755 <a href="#" id="hp<%id%>" class="hide-proposal">proposal ▿</a>\
756 <a href="#" id="dc<%id%>" class="delete-comment hidden">delete</a>\
757 <span id="cm<%id%>" class="moderation hidden">\
758 <a href="#" id="ac<%id%>" class="accept-comment">accept</a>\
761 <pre class="proposal" id="pr<%id%>">\
764 <ul class="comment-children" id="cl<%id%>"></ul>\
766 <div class="clearleft"></div>\
770 var replyTemplate
= '\
772 <div class="reply-div" id="rd<%id%>">\
773 <form id="rf<%id%>">\
774 <textarea name="comment" cols="80"></textarea>\
775 <input type="submit" value="Add reply" />\
776 <input type="button" value="Cancel" />\
777 <input type="hidden" name="parent" value="<%id%>" />\
778 <input type="hidden" name="node" value="" />\
783 $
(document
).ready(function() {
788 $
(document
).ready(function() {
789 // add comment anchors for all paragraphs that are commentable
790 $
('.sphinx-has-comment').comment();
792 // highlight search words in search results
793 $
("div.context").each(function() {
794 var params
= $
.getQueryParameters();
795 var terms
= (params
.q
) ? params
.q
[0].split(/\s
+/) : [];
796 var result
= $
(this);
797 $
.each(terms
, function() {
798 result
.highlightText(this.toLowerCase(), 'highlighted');
802 // directly open comment window if requested
803 var anchor
= document
.location
.hash
;
804 if (anchor
.substring(0, 9) == '#comment-') {
805 $
('#ao' + anchor
.substring(9)).click();
806 document
.location
.hash
= '#s' + anchor
.substring(9);