PageSourceSearch

https://deepdreamgenerator.com/assets/comments/js/commentsModule.js?v=14

js deepdreamgenerator.com collected 2026-09-24 09:33:49 UTC 26,177 bytes, 817 lines download raw bytes

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.