1let commentsModule = {}; 2 3commentsModule.popoverOptions = { 4 trigger: 'manual', 5 html: true, 6 sanitize: false 7}; 8 9commentsModule.initEmojiPicker = function (emojiMenu, commentId, isForTextarea, textareaEl) { 10 if(emojiMenu.is(":empty")) { 11 const emojiPicker = picmo.createPicker({ 12 rootElement: emojiMenu, 13 renderer: new picmoTwemoji.TwemojiRenderer(), 14 maxRecents: 16, 15 emojiSize: "3rem", 16 showVariants: false, 17 emojiVersion: 14 18 }); 19 20 emojiPicker.addEventListener("emoji:select", selection => { 21 if(isForTextarea == "yes") { 22 var currentTextareaElValue = textareaEl.val(); 23 textareaEl.val(currentTextareaElValue + selection.emoji); 24 textareaEl.trigger("input"); 25 } else { 26 commentsModule.reactOnComment("dropdown", commentId, selection.emoji); 27 } 28 }); 29 } 30}; 31 32commentsModule.parseEmojis = function() { 33 $('.dream-comment-content-paragraph, .dream-reply-content-paragraph, .dream-comments-reactions-span').each(function() { 34 emojiparser.parse(this); 35 }); 36} 37 38commentsModule.confirmAction = function ( 39 confirmModalHeader = confirmModalTitle, 40 confirmModalMessage = confirmModalMsg, 41 functionCallback, 42 withConfirmCheckbox = false, 43 checkboxType = null, 44 confirmCheckBoxText = "", 45 callback = null 46) { 47 let checkboxId = ""; 48 if (withConfirmCheckbox === true) { 49 if (checkboxType == "block") { 50 checkboxId = "hide-user-comments-" + Date.now(); 51 } else if (checkboxType == "unblock") { 52 checkboxId = "show-user-comments-" + Date.now(); 53 } 54 confirmModalMessage = confirmCheckBoxText.replace( 55 /checkBoxIdPlaceholder/g, 56 checkboxId 57 ); 58 } 59 60 return $.confirm({ 61 title: confirmModalHeader, 62 content: confirmModalMessage, 63 escapeKey: true, 64 backgroundDismiss: true, 65 buttons: { 66 Cancel: { 67 text: confirmModalCanc, 68 btnClass: "btn-default", 69 action: () => { 70 if (withConfirmCheckbox === true) { 71 callback("Cancel"); 72 } 73 }, 74 }, 75 Yes: { 76 text: confirmModalOk, 77 btnClass: "btn-orange", 78 action: () => { 79 if (withConfirmCheckbox === true) { 80 callback("Yes"); 81 82 if (typeof functionCallback === "function") { 83 functionCallback(checkboxId); 84 } 85 } else { 86 if (typeof functionCallback === "function") { 87 functionCallback(); 88 } 89 } 90 }, 91 }, 92 }, 93 onContentReady: function () { 94 $(this.$content) 95 .find('input[type="checkbox"]') 96 .attr("data-checkbox-id", checkboxId); 97 }, 98 }); 99}; 100 101commentsModule.reportModal = function ( 102 event, 103 reportCommentId, 104 modalTitle = reportModalTitle, 105 reportModalMsg = "" 106) { 107 let textareaId = "report-comment-modal-textarea-" + Date.now(); 108 let submitButtonId = "report-comment-modal-submit-button-" + Date.now(); 109 reportModalMsg = reportModalMsg.replace(/textareaIdPlaceholder/g, textareaId); 110 111 return $.confirm({ 112 title: modalTitle, 113 content: reportModalMsg, 114 escapeKey: true, 115 backgroundDismiss: true, 116 buttons: { 117 Cancel: { 118 text: reportModalClose, 119 btnClass: "btn-default", 120 }, 121 Yes: { 122 text: reportModalOk, 123 id: submitButtonId, 124 btnClass: "btn-orange submit-btn", 125 action: () => { 126 textarea = $("#" + textareaId); 127 feedback = $("#" + textareaId + "-feedback"); 128 reason = textarea.val(); 129 130 textarea.prop("disabled", true); 131 $("#" + submitButtonId).prop("disabled", true); 132 133 $.ajax({ 134 type: "POST", 135 url: "/dream-comments/report/" + reportCommentId, 136 data: { 137 reason, 138 }, 139 dataType: "json", 140 success: (response) => { 141 feedback 142 .removeClass("text-success text-danger text-warning") 143 .addClass("text-" + response.status) 144 .text(response.message); 145 if (response.status == "success") { 146 textarea.prev("label").remove(); 147 textarea.remove(); 148 $("." + submitButtonId).remove(); 149 $(event.target).remove(); 150 } else { 151 $("." + submitButtonId).prop("disabled", false); 152 } 153 textarea.prop("disabled", false); 154 }, 155 }); 156 157 return false;
158 }, 159 }, 160 }, 161 onContentReady: function () { 162 this.buttons.Yes.addClass(submitButtonId); 163 }, 164 }); 165}; 166 167/** 168 * Notifications link to the whole thread (previewSingleComment only accepts a 169 * top level comment), so on arrival we still have to walk down to the reply the 170 * notification was actually about, open the collapsed replies and point at it. 171 */ 172commentsModule.highlightTargetComment = function () { 173 var targetId = 174 typeof previewHighlightCommentId !== "undefined" 175 ? parseInt(previewHighlightCommentId, 10) 176 : 0; 177 178 if (!targetId) { 179 return; 180 } 181 182 var $target = $("#dream-comments-box").find("#dream-comment-" + targetId); 183 184 if ($target.length === 0) { 185 // Deleted, or past the pageLoadReplies rendered on load. When we are 186 // previewing one thread, open its replies anyway so the conversation and 187 // its "Load More Replies" button are visible rather than a bare comment. 188 if ( 189 typeof previewSingleComment !== "undefined" && 190 previewSingleComment === true 191 ) { 192 $("#dream-comments-box") 193 .find(".dream-comment-expand-replies") 194 .not(".active") 195 .first() 196 .trigger("click"); 197 } 198 return; 199 } 200 201 // Replies render inside a collapsed block; open it before scrolling. 202 var $repliesBox = $target.closest(".dream-comment-replies"); 203 if ($repliesBox.length) { 204 var $expandButton = $repliesBox 205 .closest(".dream-comment-content") 206 .find(".dream-comment-expand-replies"); 207 208 if ($expandButton.length && !$expandButton.hasClass("active")) { 209 $expandButton.trigger("click"); 210 } else if ($expandButton.length === 0) { 211 $repliesBox.closest(".row").show(); 212 } 213 } 214 215 $target.addClass("dream-comment-highlighted"); 216 217 // .offset() reports top:0 for an element with no layout box rather than 218 // anything falsy, so a plain truthiness check would sail through and scroll 219 // the reader to the top of the page. That is the normal state when the link 220 // is opened in a background tab, where the rAF-driven slideDown never runs. 221 var targetTop = function () { 222 var el = $target[0]; 223 if (!el || !el.getClientRects().length) { 224 return null; 225 } 226 return $target.offset().top; 227 }; 228 229 var scrollToTarget = function () { 230 var top = targetTop(); 231 if (top === null) { 232 return null; 233 } 234 $("html, body") 235 .stop(true) 236 .animate({ scrollTop: Math.max(0, top - $(window).innerHeight() / 3) }, 600); 237 return top; 238 }; 239 240 // slideDown from the expand click runs 400ms; scroll once it has settled. 241 setTimeout(function () { 242 var firstTop = scrollToTarget(); 243 if (firstTop === null) { 244 return; 245 } 246 247 // parseEmojis() injects <img class="emoji"> sized only by max-width / 248 // max-height, so each occupies 0px until it decodes and then shoves the 249 // target down. Re-aim once, but only if it actually moved. 250 setTimeout(function () { 251 var settledTop = targetTop(); 252 if (settledTop !== null && Math.abs(settledTop - firstTop) > 40) { 253 scrollToTarget(); 254 } 255 }, 1200); 256 }, 450); 257}; 258 259commentsModule.getComments = function (cursor = null) { 260 var endpointUrl = "/dream-comments/" + commentsDreamUid; 261 endpointUrl += "?cursor=" + cursor; 262 263 if (previewSingleComment === true) { 264 endpointUrl += "&previewSingleComment=" + previewSingleCommentId; 265 } 266 267 return $.ajax({ 268 type: "GET", 269 url: endpointUrl, 270 data: { 271 previewSingleComment, 272 previewSingleCommentId, 273 }, 274 dataType: "json", 275 success: (response) => { 276 $("#dream-comments-box") 277 .html(response.commentsHtml) 278 .ready(() => { 279 $(document).on( 280 "shown.bs.dropdown", 281 ".dream-comment-actions-reaction", 282 (event) => { 283 const $this = $(event.currentTarget); 284 const emojiMenu = $this.find(".dropdown-emoji-menu"); 285 286 if($this.attr("data-for-textarea") == "yes") { 287 var isForTextarea = "yes"; 288 var textareaEl = $this.closest("form").find("textarea"); 289 } else { 290 var isForTextarea = "no"; 291 var textareaEl = null; 292 } 293 294 commentsModule.initEmojiPicker(emojiMenu, $this.attr("data-comment-id"), isForTextarea, textareaEl); 295 } 296 ); 297 commentsModule.parseEmojis(); 298 commentsModule.highlightTargetComment(); 299 }); 300 $("#dream-comments-add").removeClass("hidden"); 301 }, 302 error: (response) => { 303 $("#dream-comments-box").text(loadErrorMessage); 304 }, 305 }); 306}; 307 308commentsModule.hideComment = function (commentId, type, showing) { 309 return $.ajax({ 310 type: "POST", 311 url: "/dream-comments/toggle-hidden/" + commentId, 312 dataType: "json", 313 success: function (response) { 314 if (response.status == "success") { 315 commentElement = $(".dream-comments-box").find( 316 "#dream-comment-" + commentId 317 ); 318 319 if (showing === false) { 320 commentElement.html( 321 "There should have been a " + 322 type.toLowerCase() + 323 ' here, but it was hidden from the dream author. <button type="submit" class="btn btn-sm btn-orange dream-comment-hide dream-' + 324 type.toLowerCase() +
325 '-hide" data-comment-id="' + 326 commentId + 327 '" data-currently-hidden="yes">Make ' + 328 type.toLowerCase() + 329 " public</button>" 330 ); 331 } else { 332 commentElement.text( 333 "This " + type.toLowerCase() + " will display upon a page refresh." 334 ); 335 } 336 } else { 337 $.alert({ 338 title: "", 339 escapeKey: true, 340 backgroundDismiss: true, 341 content: response.message, 342 buttons: { 343 ok: { 344 text: "OK", 345 btnClass: "btn-default", 346 }, 347 }, 348 }); 349 } 350 }, 351 }); 352}; 353 354commentsModule.deleteDreamComment = function (commentid) { 355 return $.ajax({ 356 type: "POST", 357 url: "/dream-comments/delete/" + commentid, 358 data: { 359 id: commentid, 360 }, 361 dataType: "json", 362 success: (response) => { 363 if (response.status == "success") { 364 var commentDreamUid = response.commentDreamUid; 365 var commentId = response.commentId; 366 var commentParentId = response.parentId; 367 var repliesCount = response.commentsCount; 368 var commentElementId = "#dream-comment-" + commentId; 369 var commentElement = $(commentElementId); 370 var commentHrElement = commentElement.next("hr"); 371 372 commentElement.fadeOut(500, () => { 373 commentElement.remove(); 374 commentHrElement.remove(); 375 if (parseInt(repliesCount) == 0) { 376 $( 377 "#dream-comment-" + commentParentId + "-reply-counter-box" 378 ).addClass("hidden"); 379 } 380 if (response.dreamCommentsCount === 0) { 381 pElement = $("<p>") 382 .addClass("empty-dream-comments text-muted text-center") 383 .append( 384 $("<small>").text( 385 "No comments yet. Be the first to leave a comment and share your thoughts!" 386 ) 387 ); 388 if (isFeedPage === true) { 389 $("#dream-" + commentDreamUid + "-comments").prepend(pElement); 390 } else { 391 $("#dream-comments-box").prepend(pElement); 392 } 393 } 394 }); 395 396 if (isFeedPage && commentParentId === null) { 397 let counter = commentElement 398 .closest(".content") 399 .find(".info") 400 .find(".dream-comments") 401 .find("span.number"); 402 if (/^\d+$/.test(counter.text())) { 403 let currentCount = parseInt(counter.text()); 404 let newCount = currentCount - 1; 405 counter.text(newCount); 406 } 407 } 408 } else { 409 $.alert({ 410 title: "", 411 escapeKey: true, 412 backgroundDismiss: true, 413 content: response.message, 414 buttons: { 415 ok: { 416 text: "OK", 417 btnClass: "btn-default", 418 action: function () {}, 419 }, 420 }, 421 }); 422 } 423 }, 424 }); 425}; 426 427commentsModule.blockDreamCommenter = function ( 428 userId, 429 hideAllComments, 430 commentDreamUid 431) { 432 return $.ajax({ 433 type: "POST", 434 url: "/dream-bans/" + commentDreamUid + "/block/" + userId, 435 data: { 436 hide: hideAllComments, 437 }, 438 dataType: "json", 439 success: (response) => { 440 if (response.status == "success") { 441 for (var i = 0; i < response.allIds.length; i++) { 442 var id = response.allIds[i]; 443 var elementId = "#dream-comment-" + id; 444 445 $(elementId) 446 .find(".dream-comment-block-user") 447 .html(`<i class="fa fa-ban"></i> Unblock User`); 448 $(elementId) 449 .find(".dream-comment-block-user") 450 .removeClass("dream-comment-block-user") 451 .addClass("dream-comment-unblock-user"); 452 } 453 454 if (response.hideComments) { 455 const hidedCommentsIds = response.hidedCommentIds; 456 for (const commentId of hidedCommentsIds) { 457 commentDiv = $("#dream-comment-" + commentId); 458 commentButtonBlock = $("<button>") 459 .attr({ 460 type: "submit", 461 class: "btn btn-sm btn-orange dream-comment-unblock-user", 462 "data-from-blockbox": "yes", 463 "data-user-id": userId, 464 }) 465 .text("Unblock User"); 466 467 commentDiv.html( 468 "There should have been a comment here, but it was hidden from the dream author. " + 469 commentButtonBlock.prop("outerHTML") 470 ); 471 } 472 } 473 } else { 474 $.alert({ 475 title: "", 476 escapeKey: true, 477 backgroundDismiss: true, 478 content: response.message, 479 buttons: { 480 ok: { 481 text: "OK", 482 btnClass: "btn-default", 483 action: function () {}, 484 }, 485 }, 486 }); 487 } 488 }, 489 }); 490}; 491 492commentsModule.unBlockDreamCommenter = function ( 493 userId, 494 showHidden, 495 fromBlockbox 496) { 497 return $.ajax({ 498 type: "POST", 499 url: "/dream-bans/unblock/" + userId, 500 data: { 501 showHidden, 502 }, 503 dataType: "json", 504 success: (response) => { 505 if (response.status == "success") { 506 for (var i = 0; i < response.allIds.length; i++) { 507 var id = response.allIds[i]; 508 var elementId = "#dream-comment-" + id; 509 510 if (fromBlockbox != "yes") { 511 $(elementId) 512 .find(".dream-comment-unblock-user") 513 .html(`<i class="fa fa-ban"></i> Block User`); 514 $(elementId) 515 .find(".dream-comment-unblock-user") 516 .removeClass("dream-comment-unblock-user") 517 .addClass("dream-comment-block-user"); 518 } else { 519 $(elementId).find(".dream-comment-unblock-user").remove(); 520 } 521 } 522 } else { 523 $.alert({ 524 title: "", 525 escapeKey: true, 526 backgroundDismiss: true, 527 content: response.message, 528 buttons: { 529 ok: { 530 text: "OK", 531 btnClass: "btn-default", 532 action: function () {}, 533 }, 534 }, 535 }); 536 } 537 }, 538 }); 539}; 540 541commentsModule.reactOnComment = function (from, commentId, emoji) { 542 return $.ajax({ 543 type: "POST", 544 url: "/dream-comments/react/" + commentId, 545 data: { 546 from, 547 emoji, 548 }, 549 dataType: "json", 550 success: function (response) { 551 var dreamCommentBox = $("#dream-comment-" + commentId); 552 var $btnGroup = dreamCommentBox.find(".btn-group").first(); 553 var dreamCommentContent = dreamCommentBox.find(".dream-comment-content").first(); 554 555 if (response.status == "noMoreEmojis") { 556 // Max limit of emojis for a comment reached 557 $.alert({ 558 title: "", 559 escapeKey: true, 560 backgroundDismiss: true, 561 content: response.message, 562 buttons: { 563 ok: { 564 text: "OK", 565 btnClass: "btn-danger", 566 action: function () {}, 567 }, 568 }, 569 }); 570 571 $btnGroup.removeClass("open"); 572 $btnGroup.find(".dropdown-toggle").attr("aria-expanded", false); 573 return; 574 } 575 576 if (response.status == "notChanged") { 577 // Reaction hasn't changed 578 $btnGroup.removeClass("open"); 579 $btnGroup.find(".dropdown-toggle").attr("aria-expanded", false); 580 return; 581 } 582 583 // Determine if it's a reaction for a comment or reply 584 if ($btnGroup.hasClass("btn-reply-group")) { 585 var newReactionSpan = $("<span>") 586 .addClass( 587 "dream-comments-reactions-span dream-comments-reactions-reply-span " + 588 response.reactionClass + 589 " participating" 590 ) 591 .attr("data-reaction-id", response.reactionId) 592 .attr("data-emoji", response.reaction) 593 .text(response.reaction + " ") 594 .append($("<span>").text(response.newReactionCount)); 595 } else { 596 var newReactionSpan = $("<span>") 597 .addClass( 598 "dream-comments-reactions-span " + 599 response.reactionClass + 600 " participating" 601 ) 602 .attr("data-reaction-id", response.reactionId) 603 .attr("data-emoji", response.reaction) 604 .text(response.reaction + " ") 605 .append($("<span>").text(response.newReactionCount)); 606 } 607 608 // Add popover 609 newReactionSpan.attr("data-toggle", "popover") 610 .attr("data-placement", "top") 611 .attr("title", "People who reacted") 612 .attr("data-content", response.popupHtml); 613 614 if (response.status == "successOverride") { 615 // Overriding already existing reaction 616 if (response.oldReactionCount == 0) { 617 // Remove the old reaction from the DOM if its count == 0 618 dreamCommentContent.find("." + response.oldReactionClass).remove(); 619 } else { 620 // Modify the already existing reaction in the DOM and remove participating from it 621 oldReaction = dreamCommentContent.find( 622 "." + response.oldReactionClass 623 ); 624 oldReaction.removeClass("participating"); 625 oldReaction.find("span").text(response.oldReactionCount); 626 627 oldReaction.attr("data-content", response.oldReactionPopupHtml); 628 } 629 630 if (response.newReactionCount < 2) { 631 // New reaction doesn't exist in the DOM, then insert it 632 dreamCommentContent 633 .find(".dream-comment-reactions-box") 634 .first() 635 .prepend(newReactionSpan); 636 } else { 637 // Modify the already existing reaction in the DOM 638 overrideReaction = dreamCommentContent.find( 639 "." + response.reactionClass 640 ); 641 overrideReaction.attr("data-toggle", "popover") 642 .attr("data-placement", "top") 643 .attr("title", "People who reacted") 644 .attr("data-content", response.popupHtml); 645 overrideReaction.addClass("participating"); 646 overrideReaction.attr("data-reaction-id", response.reactionId); 647 overrideReaction.find("span").text(response.newReactionCount); 648 overrideReaction.popover("hide"); 649 overrideReaction.trigger("mouseenter"); 650 } 651 } else if (response.status == "success") { 652 // New reaction must be inserted in the DOM 653 dreamCommentContent.find("." + response.reactionClass).remove(); 654 dreamCommentContent 655 .find(".dream-comment-reactions-box") 656 .first() 657 .prepend(newReactionSpan); 658 } else { 659 $.alert({ 660 title: "", 661 escapeKey: true, 662 backgroundDismiss: true, 663 content: response.message, 664 buttons: { 665 ok: { 666 text: "OK", 667 btnClass: "btn-default", 668 action: function () {}, 669 }, 670 }, 671 }); 672 } 673 674 // Hide the reaction select dropdown 675 $btnGroup.removeClass("open"); 676 $btnGroup.find(".dropdown-toggle").attr("aria-expanded", false); 677 678 // Parse the document to display emojis 679 commentsModule.parseEmojis(); 680 }, 681 }); 682}; 683 684commentsModule.checkTextarea = function (textarea, textElement) { 685 const content = textarea.val(); 686 const charactersLength = Array.from(content).length; 687 const textareaMinLength = parseInt(window.textareaMinLength); 688 const textareaMaxLength = parseInt(window.textareaMaxLength); 689 const remainingCharacters = textareaMaxLength - charactersLength; 690 var isFeedPage = window.isFeedPage; 691 692 textarea.removeAttr("style"); 693 textElement.removeClass("text-danger"); 694 if (isFeedPage === true) { 695 textarea 696 .closest("form") 697 .find(".dream-comments-add-form-submit-row") 698 .removeAttr("style"); 699 } 700 701 if (charactersLength < textareaMinLength) { 702 textarea.css("border-color", "red"); 703 if (isFeedPage === true) { 704 textarea 705 .closest("form") 706 .find(".dream-comments-add-form-submit-row") 707 .css("border-color", "red"); 708 } 709 } else if (remainingCharacters === 0) { 710 } else if (remainingCharacters < 0) { 711 textarea.css("border-color", "red"); 712 textElement.addClass("text-danger"); 713 if (isFeedPage === true) { 714 textarea 715 .closest("form") 716 .find(".dream-comments-add-form-submit-row") 717 .css("border-color", "red"); 718 } 719 } 720 721 textElement.text(`${remainingCharacters} characters`); 722}; 723 724// ââ Editing your own comment (feedback #15) âââââââââââââââââââââââââ 725// The comment's own paragraph, not one of its replies' (a comment holds its 726// replies inside it). 727commentsModule.ownParagraph = function (commentId) { 728 return $("#dream-comment-" + commentId) 729 .children(".dream-comment-content") 730 .children("p.dream-comment-content-paragraph, p.dream-reply-content-paragraph"); 731}; 732 733commentsModule.startEdit = function (commentId) { 734 var $paragraph = commentsModule.ownParagraph(commentId); 735 if (!$paragraph.length || $paragraph.next(".dream-comment-edit-box").length) return; 736 737 var $box = $( 738 '<div class="dream-comment-edit-box">' + 739 '<textarea class="form-control dream-comment-edit-textarea" rows="3"></textarea>' + 740 '<div class="dream-comment-edit-actions">' + 741 '<button type="button" class="btn btn-sm btn-orange dream-comment-edit-save">Save</button>' + 742 '<button type="button" class="btn btn-sm btn-default dream-comment-edit-cancel">Cancel</button>' + 743 "</div>" + 744 "</div>" 745 ); 746 // The words as typed: on screen the emoji are already pictures. No 747 // maxlength: a browser counts an emoji as two against it, the server (and 748 // the post box) as one, so a comment that was fine to post could open too 749 // long to type into. The server checks the length as it does for posting. 750 $box.find("textarea").val($paragraph.attr("data-raw") || ""); 751 $box.attr("data-comment-id", commentId); 752 753 $paragraph.hide().after($box); 754 var textarea = $box.find("textarea")[0]; 755 textarea.focus(); 756 textarea.setSelectionRange(textarea.value.length, textarea.value.length); 757}; 758 759commentsModule.cancelEdit = function (commentId) { 760 var $paragraph = commentsModule.ownParagraph(commentId); 761 $paragraph.next(".dream-comment-edit-box").remove(); 762 $paragraph.show(); 763}; 764 765commentsModule.saveEdit = function (commentId) { 766 var $paragraph = commentsModule.ownParagraph(commentId); 767 var $box = $paragraph.next(".dream-comment-edit-box"); 768 if (!$box.length || $box.hasClass("saving")) return; 769 770 $box.addClass("saving").find("textarea, button").prop("disabled", true); 771 772 return $.ajax({ 773 type: "POST", 774 url: "/dream-comments/edit/" + commentId, 775 data: { content: $box.find("textarea").val() }, 776 dataType: "json", 777 success: function (response) { 778 if (response.status === "success") { 779 $paragraph.html(response.html).attr("data-raw", response.raw); 780 if (response.edited) { 781 var $time = $("#dream-comment-" + commentId).children(".dream-comment-info").find("time").first(); 782 if (!$time.next(".dream-comment-edited").length) { 783 $time.after('<span class="dream-comment-edited"> · edited</span>'); 784 } 785 } 786 commentsModule.cancelEdit(commentId); 787 commentsModule.parseEmojis(); 788 return; 789 } 790 791 $box.removeClass("saving").find("textarea, button").prop("disabled", false); 792 $.alert({ 793 title: "", 794 escapeKey: true, 795 backgroundDismiss: true, 796 content: response.message, 797 buttons: { ok: { text: "OK", btnClass: "btn-default", action: function () {} } }, 798 }); 799 }, 800 error: function (xhr) { 801 // A 419 is retried by app.js with a fresh CSRF token, and that retry 802 // calls success above. Saying "something went wrong" here first would 803 // be wrong, and re-enabling Save would invite a second request. 804 // If the retry itself fails (_csrfRetried set on it), fall through. 805 if (xhr && xhr.status === 419 && !this._csrfRetried) return; 806 $box.removeClass("saving").find("textarea, button").prop("disabled", false); 807 $.alert({ 808 title: "", 809 escapeKey: true, 810 backgroundDismiss: true, 811 content: typeof dangerMessage !== "undefined" ? dangerMessage : "Oops! Something went wrong. Please try again.", 812 buttons: { ok: { text: "OK", btnClass: "btn-default", a
812ction: function () {} } }, 813 }); 814 }, 815 }); 816}; 817
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.