notes.js 15.6 KB
Newer Older
1
var NoteList = {
gitlabhq's avatar
gitlabhq committed
2

3 4 5 6
  notes_path: null,
  target_params: null,
  target_id: 0,
  target_type: null,
7
  top_id: 0,
8 9
  bottom_id: 0,
  loading_more_disabled: false,
10
  reversed: false,
11

Riyad Preukschas's avatar
Riyad Preukschas committed
12
  init: function(tid, tt, path) {
13 14 15 16 17 18
    NoteList.notes_path = path + ".js";
    NoteList.target_id = tid;
    NoteList.target_type = tt;
    NoteList.reversed = $("#notes-list").is(".reversed");
    NoteList.target_params = "target_type=" + NoteList.target_type + "&target_id=" + NoteList.target_id;

19 20
    NoteList.setupMainTargetNoteForm();

21
    if(NoteList.reversed) {
22 23
      var form = $(".js-main-target-form");
      form.find(".buttons, .note_options").hide();
24
      var textarea = form.find(".js-note-text");
Riyad Preukschas's avatar
Riyad Preukschas committed
25 26
      textarea.css("height", "40px");
      textarea.on("focus", function(){
27
        textarea.css("height", "80px");
28
        form.find(".buttons, .note_options").show();
Riyad Preukschas's avatar
Riyad Preukschas committed
29 30
      });
    }
Riyad Preukschas's avatar
Riyad Preukschas committed
31

Riyad Preukschas's avatar
Riyad Preukschas committed
32
    // get initial set of notes
33
    NoteList.getContent();
34

35
    // add a new diff note
Riyad Preukschas's avatar
Riyad Preukschas committed
36 37 38 39
    $(document).on("click",
                    ".js-add-diff-note-button",
                    NoteList.addDiffNote);

40
    // reply to diff/discussion notes
Riyad Preukschas's avatar
Riyad Preukschas committed
41 42 43 44
    $(document).on("click",
                    ".js-discussion-reply-button",
                    NoteList.replyToDiscussionNote);

45 46 47 48 49
    // setup note preview
    $(document).on("click",
                    ".js-note-preview-button",
                    NoteList.previewNote);

50 51 52 53 54
    // update the file name when an attachment is selected
    $(document).on("change",
                   ".js-note-attachment-input",
                   NoteList.updateFormAttachment);

Riyad Preukschas's avatar
Riyad Preukschas committed
55 56 57 58 59 60 61 62 63 64
    // hide diff note form
    $(document).on("click",
                    ".js-close-discussion-note-form",
                    NoteList.removeDiscussionNoteForm);

    // remove a note (in general)
    $(document).on("click",
                    ".js-note-delete",
                    NoteList.removeNote);

65
    // reset main target form after submit
Riyad Preukschas's avatar
Riyad Preukschas committed
66 67
    $(document).on("ajax:complete",
                   ".js-main-target-form",
68
                   NoteList.resetMainTargetForm);
gitlabhq's avatar
gitlabhq committed
69

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
70

71 72 73
    $(document).on("click",
      ".js-choose-note-attachment-button",
      NoteList.chooseNoteAttachment);
74 75 76 77

    $(document).on("click",
      ".js-show-outdated-discussion",
      function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() });
78
  },
79 80


Riyad Preukschas's avatar
Riyad Preukschas committed
81
  /**
82
   * When clicking on buttons
Riyad Preukschas's avatar
Riyad Preukschas committed
83 84
   */

85 86 87 88 89 90 91
  /**
   * Called when clicking on the "add a comment" button on the side of a diff line.
   *
   * Inserts a temporary row for the form below the line.
   * Sets up the form and shows it.
   */
  addDiffNote: function(e) {
92 93
    e.preventDefault();

94
    // find the form
95
    var form = $(".js-new-note-form");
96 97 98 99 100
    var row = $(this).closest("tr");
    var nextRow = row.next();

    // does it already have notes?
    if (nextRow.is(".notes_holder")) {
Riyad Preukschas's avatar
Riyad Preukschas committed
101 102
      $.proxy(NoteList.replyToDiscussionNote,
              nextRow.find(".js-discussion-reply-button")
103 104 105 106 107 108 109
             ).call();
    } else {
      // add a notes row and insert the form
      row.after('<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"></td></tr>');
      form.clone().appendTo(row.next().find(".notes_content"));

      // show the form
Riyad Preukschas's avatar
Riyad Preukschas committed
110
      NoteList.setupDiscussionNoteForm($(this), row.next().find("form"));
111 112 113
    }
  },

114 115 116 117 118 119 120 121 122 123 124
  /**
   * Called when clicking the "Choose File" button.
   * 
   * Opesn the file selection dialog.
   */
  chooseNoteAttachment: function() {
    var form = $(this).closest("form");

    form.find(".js-note-attachment-input").click();
  },

125 126 127 128 129 130 131 132
  /**
   * Shows the note preview.
   *
   * Lets the server render GFM into Html and displays it.
   *
   * Note: uses the Toggler behavior to toggle preview/edit views/buttons
   */
  previewNote: function(e) {
133 134
    e.preventDefault();

135 136
    var form = $(this).closest("form");
    var preview = form.find('.js-note-preview');
137
    var noteText = form.find('.js-note-text').val();
138

139
    if(noteText.trim().length === 0) {
140
      preview.text('Nothing to preview.');
141
    } else {
142
      preview.text('Loading...');
143
      $.post($(this).data('url'), {note: noteText})
144 145 146 147 148 149
        .success(function(previewData) {
          preview.html(previewData);
        });
    }
  },

150 151 152 153 154 155
  /**
   * Called in response to "cancel" on a diff note form.
   * 
   * Shows the reply button again.
   * Removes the form and if necessary it's temporary row.
   */
156
  removeDiscussionNoteForm: function() {
157 158 159 160
    var form = $(this).closest("form");
    var row = form.closest("tr");

    // show the reply button (will only work for replys)
Riyad Preukschas's avatar
Riyad Preukschas committed
161
    form.prev(".js-discussion-reply-button").show();
162 163

    if (row.is(".js-temp-notes-holder")) {
Riyad Preukschas's avatar
Riyad Preukschas committed
164
      // remove temporary row for diff lines
165 166 167 168 169 170 171 172 173 174 175
      row.remove();
    } else {
      // only remove the form
      form.remove();
    }
  },

  /**
   * Called in response to deleting a note of any kind.
   *
   * Removes the actual note from view.
176
   * Removes the whole discussion if the last note is being removed.
177 178
   */
  removeNote: function() {
179 180 181 182 183 184 185 186 187 188 189 190 191
    var note = $(this).closest(".note");
    var notes = note.closest(".notes");

    // check if this is the last note for this line
    if (notes.find(".note").length === 1) {
      // for discussions
      notes.closest(".discussion").remove();

      // for diff lines
      notes.closest("tr").remove();
    }

    note.remove();
192 193 194 195 196 197 198 199
    NoteList.updateVotes();
  },

  /**
   * Called when clicking on the "reply" button for a diff line.
   *
   * Shows the note form below the notes.
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
200
  replyToDiscussionNote: function() {
201
    // find the form
202
    var form = $(".js-new-note-form");
203 204 205 206 207 208 209

    // hide reply button
    $(this).hide();
    // insert the form after the button
    form.clone().insertAfter($(this));

    // show the form
Riyad Preukschas's avatar
Riyad Preukschas committed
210
    NoteList.setupDiscussionNoteForm($(this), $(this).next("form"));
211 212
  },

213

214
  /**
215 216 217 218
   * Helper for inserting and setting up note forms.
   */


Riyad Preukschas's avatar
Riyad Preukschas committed
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
  /**
   * Called in response to creating a note failing validation.
   *
   * Adds the rendered errors to the respective form.
   * If "discussionId" is null or undefined, the main target form is assumed.
   */
  errorsOnForm: function(errorsHtml, discussionId) {
    // find the form
    if (discussionId) {
      var form = $("form[rel='"+discussionId+"']");
    } else {
      var form = $(".js-main-target-form");
    }

    form.find(".js-errors").remove();
    form.prepend(errorsHtml);

    form.find(".js-note-text").focus();
  },


240 241
  /**
   * Shows the diff/discussion form and does some setup on it.
242 243 244
   *
   * Sets some hidden fields in the form.
   *
245 246
   * Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
   *       and "noteableId" data attributes set.
247
   */
248
  setupDiscussionNoteForm: function(dataHolder, form) {
249
    // setup note target
250
    form.attr("rel", dataHolder.data("discussionId"));
251
    form.find("#note_commit_id").val(dataHolder.data("commitId"));
252 253 254
    form.find("#note_line_code").val(dataHolder.data("lineCode"));
    form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
    form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
255

256
    NoteList.setupNoteForm(form);
257

258
    form.find(".js-note-text").focus();
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
  },

  /**
   * Shows the main form and does some setup on it.
   *
   * Sets some hidden fields in the form.
   */
  setupMainTargetNoteForm: function() {
    // find the form
    var form = $(".js-new-note-form");
    // insert the form after the button
    form.clone().replaceAll($(".js-main-target-form"));

    form = form.prev("form");

    // show the form
    NoteList.setupNoteForm(form);

    // fix classes
    form.removeClass("js-new-note-form");
    form.addClass("js-main-target-form");

    // remove unnecessary fields and buttons
    form.find("#note_line_code").remove();
    form.find(".js-close-discussion-note-form").remove();
  },

  /**
   * General note form setup.
   *
   * * deactivates the submit button when text is empty
   * * hides the preview button when text is empty
   * * setup GFM auto complete
   * * show the form
   */
  setupNoteForm: function(form) {
    disableButtonIfEmptyField(form.find(".js-note-text"), form.find(".js-comment-button"));

297 298
    form.removeClass("js-new-note-form");

299
    // setup preview buttons
300 301
    form.find(".js-note-edit-button, .js-note-preview-button")
        .tooltip({ placement: 'left' });
302

303
    previewButton = form.find(".js-note-preview-button");
304 305
    form.find(".js-note-text").on("input", function() {
      if ($(this).val().trim() !== "") {
306
        previewButton.removeClass("turn-off").addClass("turn-on");
307
      } else {
308
        previewButton.removeClass("turn-on").addClass("turn-off");
309 310 311
      }
    });

312 313 314 315 316
    // remove notify commit author checkbox for non-commit notes
    if (form.find("#note_noteable_type").val() !== "Commit") {
      form.find(".js-notify-commit-author").remove();
    }

317
    GitLab.GfmAutoComplete.setup();
318 319 320 321 322

    form.show();
  },


323
  /**
324 325
   * Handle loading the initial set of notes.
   * And set up loading more notes when scrolling to the bottom of the page.
326
   */
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
327 328


329
  /**
330
   * Gets an inital set of notes.
331
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
332 333
  getContent: function() {
    $.ajax({
334 335
      url: NoteList.notes_path,
      data: NoteList.target_params,
336 337
      complete: function(){ $('.js-notes-busy').removeClass("loading")},
      beforeSend: function() { $('.js-notes-busy').addClass("loading") },
Riyad Preukschas's avatar
Riyad Preukschas committed
338 339 340
      dataType: "script"
    });
  },
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
341

342 343 344 345
  /**
   * Called in response to getContent().
   * Replaces the content of #notes-list with the given html.
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
346
  setContent: function(newNoteIds, html) {
347 348
    NoteList.top_id = newNoteIds.first();
    NoteList.bottom_id = newNoteIds.last();
Riyad Preukschas's avatar
Riyad Preukschas committed
349 350
    $("#notes-list").html(html);

Riyad Preukschas's avatar
Riyad Preukschas committed
351
    // for the wall
352
    if (NoteList.reversed) {
Riyad Preukschas's avatar
Riyad Preukschas committed
353
      // init infinite scrolling
354
      NoteList.initLoadMore();
Riyad Preukschas's avatar
Riyad Preukschas committed
355 356

      // init getting new notes
357
      NoteList.initRefreshNew();
Riyad Preukschas's avatar
Riyad Preukschas committed
358 359
    }
  },
360 361 362


  /**
363
   * Handle loading more notes when scrolling to the bottom of the page.
364
   * The id of the last note in the list is in NoteList.bottom_id.
365
   *
366
   * Set up refreshing only new notes after all notes have been loaded.
367
   */
368 369 370 371 372


  /**
   * Initializes loading more notes when scrolling to the bottom of the page.
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
373 374 375 376 377 378 379 380 381 382 383 384 385
  initLoadMore: function() {
    $(document).endlessScroll({
      bottomPixels: 400,
      fireDelay: 1000,
      fireOnce:true,
      ceaseFire: function() {
        return NoteList.loading_more_disabled;
      },
      callback: function(i) {
        NoteList.getMore();
      }
    });
  },
386 387 388 389

  /**
   * Gets an additional set of notes.
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
390 391 392 393
  getMore: function() {
    // only load more notes if there are no "new" notes
    $('.loading').show();
    $.ajax({
394 395
      url: NoteList.notes_path,
      data: NoteList.target_params + "&loading_more=1&" + (NoteList.reversed ? "before_id" : "after_id") + "=" + NoteList.bottom_id,
396 397
      complete: function(){ $('.js-notes-busy').removeClass("loading")},
      beforeSend: function() { $('.js-notes-busy').addClass("loading") },
Riyad Preukschas's avatar
Riyad Preukschas committed
398 399 400
      dataType: "script"
    });
  },
401

402 403 404 405
  /**
   * Called in response to getMore().
   * Append notes to #notes-list.
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
406 407
  appendMoreNotes: function(newNoteIds, html) {
    var lastNewNoteId = newNoteIds.last();
408 409
    if(lastNewNoteId != NoteList.bottom_id) {
      NoteList.bottom_id = lastNewNoteId;
Riyad Preukschas's avatar
Riyad Preukschas committed
410 411 412
      $("#notes-list").append(html);
    }
  },
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
413

414 415 416 417
  /**
   * Called in response to getMore().
   * Disables loading more notes when scrolling to the bottom of the page.
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
418
  finishedLoadingMore: function() {
419
    NoteList.loading_more_disabled = true;
420

Riyad Preukschas's avatar
Riyad Preukschas committed
421
    // make sure we are up to date
422
    NoteList.updateVotes();
Riyad Preukschas's avatar
Riyad Preukschas committed
423
  },
424 425 426 427 428 429 430


  /**
   * Handle refreshing and adding of new notes.
   *
   * New notes are all notes that are created after the site has been loaded.
   * The "old" notes are in #notes-list the "new" ones will be in #new-notes-list.
431
   * The id of the last "old" note is in NoteList.bottom_id.
432 433 434 435 436
   */


  /**
   * Initializes getting new notes every n seconds.
Riyad Preukschas's avatar
Riyad Preukschas committed
437 438
   *
   * Note: only used on wall.
439
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
440 441 442
  initRefreshNew: function() {
    setInterval("NoteList.getNew()", 10000);
  },
443 444

  /**
445
   * Gets the new set of notes.
Riyad Preukschas's avatar
Riyad Preukschas committed
446 447
   *
   * Note: only used on wall.
448
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
449 450
  getNew: function() {
    $.ajax({
451 452
      url: NoteList.notes_path,
      data: NoteList.target_params + "&loading_new=1&after_id=" + (NoteList.reversed ? NoteList.top_id : NoteList.bottom_id),
Riyad Preukschas's avatar
Riyad Preukschas committed
453 454 455
      dataType: "script"
    });
  },
456 457 458 459

  /**
   * Called in response to getNew().
   * Replaces the content of #new-notes-list with the given html.
Riyad Preukschas's avatar
Riyad Preukschas committed
460 461
   *
   * Note: only used on wall.
462
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
463 464
  replaceNewNotes: function(newNoteIds, html) {
    $("#new-notes-list").html(html);
465
    NoteList.updateVotes();
Riyad Preukschas's avatar
Riyad Preukschas committed
466
  },
467 468

  /**
Riyad Preukschas's avatar
Riyad Preukschas committed
469
   * Adds a single common note to #notes-list.
470
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
471
  appendNewNote: function(id, html) {
472 473
    $("#notes-list").append(html);
    NoteList.updateVotes();
Riyad Preukschas's avatar
Riyad Preukschas committed
474
  },
475

476
  /**
Riyad Preukschas's avatar
Riyad Preukschas committed
477
   * Adds a single discussion note to #notes-list.
Riyad Preukschas's avatar
Riyad Preukschas committed
478 479
   *
   * Also removes the corresponding form.
480
   */
481
  appendNewDiscussionNote: function(discussionId, diffRowHtml, noteHtml) {
Riyad Preukschas's avatar
Riyad Preukschas committed
482 483 484
    var form = $("form[rel='"+discussionId+"']");
    var row = form.closest("tr");

485 486
    // is this the first note of discussion?
    if (row.is(".js-temp-notes-holder")) {
Riyad Preukschas's avatar
Riyad Preukschas committed
487
      // insert the note and the reply button after the temp row
488
      row.after(diffRowHtml);
Riyad Preukschas's avatar
Riyad Preukschas committed
489
      // remove the note (will be added again below)
490 491 492 493 494
      row.next().find(".note").remove();
    }

    // append new note to all matching discussions
    $(".notes[rel='"+discussionId+"']").append(noteHtml);
Riyad Preukschas's avatar
Riyad Preukschas committed
495 496 497

    // cleanup after successfully creating a diff/discussion note
    $.proxy(NoteList.removeDiscussionNoteForm, form).call();
498 499
  },

Riyad Preukschas's avatar
Riyad Preukschas committed
500 501 502 503 504 505 506 507 508
  /**
   * Adds a single wall note to #new-notes-list.
   *
   * Note: only used on wall.
   */
  appendNewWallNote: function(id, html) {
    $("#new-notes-list").prepend(html);
  },

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
  /**
   * Called in response the main target form has been successfully submitted.
   *
   * Removes any errors.
   * Resets text and preview.
   * Resets buttons.
   */
  resetMainTargetForm: function(){
    var form = $(this);

    // remove validation errors
    form.find(".js-errors").remove();

    // reset text and preview
    var previewContainer = form.find(".js-toggler-container.note_text_and_preview");
    if (previewContainer.is(".on")) {
      previewContainer.removeClass("on");
    }
    form.find(".js-note-text").val("").trigger("input");
  },

  /**
   * Called after an attachment file has been selected.
   *
   * Updates the file name for the selected attachment.
   */
  updateFormAttachment: function() {
    var form = $(this).closest("form");

    // get only the basename
    var filename = $(this).val().replace(/^.*[\\\/]/, '');

    form.find(".js-attachment-filename").text(filename);
  },

544 545 546 547 548 549 550 551
  /**
   * Recalculates the votes and updates them (if they are displayed at all).
   *
   * Assumes all relevant notes are displayed (i.e. there are no more notes to
   * load via getMore()).
   * Might produce inaccurate results when not all notes have been loaded and a
   * recalculation is triggered (e.g. when deleting a note).
   */
Riyad Preukschas's avatar
Riyad Preukschas committed
552 553
  updateVotes: function() {
    var votes = $("#votes .votes");
554
    var notes = $("#notes-list .note .vote");
Riyad Preukschas's avatar
Riyad Preukschas committed
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569

    // only update if there is a vote display
    if (votes.size()) {
      var upvotes = notes.filter(".upvote").size();
      var downvotes = notes.filter(".downvote").size();
      var votesCount = upvotes + downvotes;
      var upvotesPercent = votesCount ? (100.0 / votesCount * upvotes) : 0;
      var downvotesPercent = votesCount ? (100.0 - upvotesPercent) : 0;

      // change vote bar lengths
      votes.find(".bar-success").css("width", upvotesPercent+"%");
      votes.find(".bar-danger").css("width", downvotesPercent+"%");
      // replace vote numbers
      votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes));
      votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));
570
    }
Riyad Preukschas's avatar
Riyad Preukschas committed
571
  }
572
};