MozReview: Copy commentDialogView.js in anticipation of updated view. (Bug 1272630) r?glob draft
authorDavid Walsh <dwalsh@mozilla.com>
Fri, 05 Aug 2016 11:11:17 -0500
changeset 74 4bf59933660ea86a673c53b1937f9c694b561e69
parent 73 677d00d65abc6a8494f56438c1ad91afed69f114
child 75 82baa26309fb2d2dcf01bc3332796c2ba61134b3
push idunknown
push userunknown
push dateunknown
reviewersglob
bugs1272630
MozReview: Copy commentDialogView.js in anticipation of updated view. (Bug 1272630) r?glob MozReview-Commit-ID: EQnJmU8pF9J
reviewboard/reviewboard/static/rb/js/views/commentDialogView_mozreview.js
reviewboard/reviewboard/staticbundles.py
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/views/commentDialogView_mozreview.js
@@ -0,0 +1,682 @@
+/*
+ * Displays a list of existing comments within a comment dialog.
+ *
+ * Each comment in the list is an existing, published comment that a user
+ * has made. They will be displayed, along with any issue states and
+ * identifying information, and links for viewing the comment on the review
+ * or replying to it.
+ *
+ * This is used internally in CommentDialogView.
+ */
+var CommentsListView = Backbone.View.extend({
+    itemTemplate: _.template([
+        '<li class="<%= itemClass %>">',
+         '<h2>',
+          '<%- comment.user.name %>',
+          '<span class="actions">',
+           '<a class="comment-list-view-action" href="<%= comment.url %>"><%- viewText %></a>',
+           '<a class="comment-list-reply-action" href="<%= reviewRequestURL %>',
+                    '?reply_id=<%= comment.reply_to_id || comment.comment_id %>',
+                    '&reply_type=<%= replyType %>">Reply</a>',
+          '</span>',
+         '</h2>',
+         '<pre><%- comment.text %></pre>'
+    ].join('')),
+
+    /*
+     * Set the list of displayed comments.
+     */
+    setComments: function(comments, replyType) {
+        var reviewRequestURL = this.options.reviewRequestURL,
+            commentIssueManager = this.options.commentIssueManager,
+            interactive = this.options.issuesInteractive,
+            odd = true,
+            $items = $();
+
+        if (comments.length === 0) {
+            return;
+        }
+
+        _.each(comments, function(serializedComment) {
+            var commentID = serializedComment.comment_id,
+                $item = $(this.itemTemplate({
+                    comment: serializedComment,
+                    itemClass: odd ? 'odd' : 'even',
+                    reviewRequestURL: reviewRequestURL,
+                    replyType: replyType,
+                    viewText: gettext('View')
+                })),
+                commentIssueBar;
+
+            if (serializedComment.issue_opened) {
+                commentIssueBar = new RB.CommentIssueBarView({
+                    reviewID: serializedComment.review_id,
+                    commentID: commentID,
+                    commentType: replyType,
+                    issueStatus: serializedComment.issue_status,
+                    interactive: interactive,
+                    commentIssueManager: commentIssueManager
+                });
+                commentIssueBar.render().$el.appendTo($item);
+
+                /*
+                 * Update the serialized comment's issue status whenever
+                 * the real comment's status is changed so we will
+                 * display it correctly the next time we render it.
+                 */
+                commentIssueManager.on('issueStatusUpdated',
+                                       function(comment) {
+                    if (comment.id === commentID) {
+                        serializedComment.issue_status =
+                            comment.get('issueStatus');
+                    }
+                });
+            }
+
+            $items = $items.add($item);
+
+            odd = !odd;
+        }, this);
+
+        this.$el
+            .empty()
+            .append($items);
+    }
+});
+
+
+/*
+ * A dialog that allows for creating, editing or deleting draft comments on
+ * a diff or file attachment. The dialog can be moved around on the page.
+ *
+ * Any existing comments for the selected region will be displayed alongside
+ * the dialog for reference. However, this dialog is not intended to be
+ * used to reply to those comments.
+ */
+RB.CommentDialogView = Backbone.View.extend({
+    DIALOG_TOTAL_HEIGHT: 350,
+    DIALOG_NON_EDITABLE_HEIGHT: 120,
+    SLIDE_DISTANCE: 10,
+    COMMENTS_BOX_WIDTH: 280,
+    FORM_BOX_WIDTH: 450,
+
+    className: 'comment-dlg',
+    template: _.template([
+        '<div class="other-comments">',
+        ' <h1 class="title"><%- otherReviewsText %></h1>',
+        ' <ul></ul>',
+        '</div>',
+        '<form method="post">',
+        ' <h1 class="comment-dlg-header">',
+        '  <span class="title"></span>',
+        '<% if (authenticated && !hasDraft) { %>',
+        '  <a class="markdown-info" href="<%- markdownDocsURL %>"',
+        '     target="_blank"><%- markdownText %></a>',
+        '<% } %>',
+        ' </h1>',
+        '<% if (!authenticated) { %>',
+        ' <p class="login-text">',
+        '  <%= loginText %>',
+        ' </p>',
+        '<% } else if (hasDraft) { %>',
+        ' <p class="draft-warning"><%= draftWarning %></p>',
+        '<% } %>',
+        ' <div class="comment-dlg-body">',
+        '  <div class="comment-text-field"></div>',
+        '  <ul class="comment-dlg-options">',
+        '   <li class="comment-issue-options">',
+        '    <input type="checkbox" id="comment_issue" />',
+        '    <label for="comment_issue" accesskey="i"><%= openAnIssueText %></label>',
+        '   </li>',
+        '   <li class="comment-markdown-options">',
+        '    <input type="checkbox" id="enable_markdown" />',
+        '    <label for="enable_markdown" accesskey="m"><%= enableMarkdownText %></label>',
+        '   </li>',
+        '  </ul>',
+        ' </div>',
+        ' <div class="comment-dlg-footer">',
+        '  <div class="buttons">',
+        '   <input type="button" class="save" value="<%- saveButton %>" ',
+        '          disabled="true" />',
+        '   <input type="button" class="cancel" value="<%- cancelButton %>" />',
+        '   <input type="button" class="delete" value="<%- deleteButton %>" ',
+        '          disabled="true" />',
+        '   <input type="button" class="close" value="<%- closeButton %>" />',
+        '  </div>',
+        ' </div>',
+        '</form>'
+    ].join('')),
+
+    events: {
+        'click .buttons .cancel': '_onCancelClicked',
+        'click .buttons .close': '_onCancelClicked',
+        'click .buttons .delete': '_onDeleteClicked',
+        'click .buttons .save': 'save',
+        'keydown .comment-text-field': '_onTextKeyDown'
+    },
+
+    initialize: function() {
+    },
+
+    render: function() {
+        var userSession = RB.UserSession.instance,
+            reviewRequest = this.model.get('reviewRequest'),
+            reviewRequestEditor = this.model.get('reviewRequestEditor');
+
+        this.options.animate = (this.options.animate !== false);
+
+        this.$el
+            .hide()
+            .html(this.template({
+                authenticated: userSession.get('authenticated'),
+                hasDraft: reviewRequest.get('hasDraft'),
+                markdownDocsURL: MANUAL_URL + 'users/markdown/',
+                markdownText: gettext('Markdown'),
+                otherReviewsText: gettext('Other reviews'),
+                loginText: interpolate(
+                    gettext('You must <a href="%s">log in</a> to post a comment.'),
+                    [userSession.get('loginURL')]),
+                draftWarning: interpolate(
+                    gettext('The review request\'s current <a href="%s">draft</a> needs to be published before you can comment.'),
+                    [reviewRequest.get('reviewURL')]),
+                openAnIssueText: gettext('Open an <u>I</u>ssue'),
+                enableMarkdownText: gettext('Enable <u>M</u>arkdown'),
+                saveButton: gettext('Save'),
+                cancelButton: gettext('Cancel'),
+                deleteButton: gettext('Delete'),
+                closeButton: gettext('Close')
+            }));
+
+        this._$commentsPane = this.$('.other-comments');
+        this._$draftForm = this.$('form');
+        this._$body = this._$draftForm.children('.comment-dlg-body');
+        this._$header = this._$draftForm.children('.comment-dlg-header');
+        this._$footer = this._$draftForm.children('.comment-dlg-footer');
+        this._$title = this._$header.children('.title');
+
+        this._$commentOptions = this._$body.children('.comment-dlg-options');
+
+        this._$issueOptions =
+            this._$commentOptions.children(".comment-issue-options")
+                .bindVisibility(this.model, 'canEdit');
+        this._$markdownOptions =
+            this._$commentOptions.children(".comment-markdown-options")
+                .bindVisibility(this.model, 'canEdit');
+
+        this._$issueField = this._$issueOptions.find('input')
+            .bindProperty('checked', this.model, 'openIssue')
+            .bindProperty('disabled', this.model, 'editing', {
+                elementToModel: false,
+                inverse: true
+            });
+
+        this._$enableMarkdownField = this._$markdownOptions.find('input')
+            .bindProperty('checked', this.model, 'richText')
+            .bindProperty('disabled', this.model, 'editing', {
+                elementToModel: false,
+                inverse: true
+            });
+
+        this.$buttons = this._$footer.find('.buttons');
+
+        this.$saveButton = this.$buttons.find('input.save')
+            .bindVisibility(this.model, 'canEdit')
+            .bindProperty('disabled', this.model, 'canSave', {
+                elementToModel: false,
+                inverse: true
+            });
+
+        this.$cancelButton = this.$buttons.find('input.cancel')
+            .bindVisibility(this.model, 'canEdit');
+
+        this.$deleteButton = this.$buttons.find('input.delete')
+            .bindVisibility(this.model, 'canDelete')
+            .bindProperty('disabled', this.model, 'canDelete', {
+                elementToModel: false,
+                inverse: true
+            });
+
+        this.$closeButton = this.$buttons.find('input.close')
+            .bindVisibility(this.model, 'canEdit', {
+                inverse: true
+            });
+
+        this.commentsList = new CommentsListView({
+            el: this._$commentsPane.find('ul'),
+            reviewRequestURL: reviewRequest.get('reviewURL'),
+            commentIssueManager: this.options.commentIssueManager,
+            issuesInteractive: reviewRequestEditor.get('editable')
+        });
+
+        /*
+         * We need to handle keypress here, rather than in events above,
+         * because jQuery will actually handle it. Backbone fails to.
+         */
+        this._textEditor = new RB.TextEditorView({
+            el: this._$draftForm.find('.comment-text-field'),
+            autoSize: false,
+            minHeight: 0,
+            text: this.model.get('text'),
+            bindRichText: {
+                model: this.model,
+                attrName: 'richText'
+            }
+        });
+        this._textEditor.render();
+        this._textEditor.show();
+        this._textEditor.$el
+            .keypress(_.bind(this._onTextKeyPress, this))
+            .bindVisibility(this.model, 'canEdit');
+        this._textEditor.on('change', function() {
+            this.model.set('text', this._textEditor.getText());
+        }, this);
+        this._textEditor.bindRichTextCheckbox(this._$enableMarkdownField);
+        this._textEditor.bindRichTextVisibility(
+            this._$draftForm.find('.markdown-info'));
+
+        this.listenTo(this.model, 'change:text', function() {
+            this._textEditor.setText(this.model.get('text'));
+        });
+
+        this.listenTo(this.model, 'change:richText', this._handleResize);
+
+        this.$el
+            .css("position", "absolute")
+            .mousedown(function(evt) {
+                /*
+                 * Prevent this from reaching the selection area, which will
+                 * swallow the default action for the mouse down.
+                 */
+                evt.stopPropagation();
+            })
+            .resizable({
+                handles: $.support.touch ? "grip,se"
+                                         : "grip,n,e,s,w,se,sw,ne,nw",
+                transparent: true,
+                resize: _.bind(this._handleResize, this)
+            })
+            .proxyTouchEvents();
+
+        this._$header.css('cursor', 'move');
+        this.$el.draggable({
+            handle: '.comment-dlg-header'
+        });
+
+        this.listenTo(this.model, 'change:dirty', this._updateTitle);
+        this._updateTitle();
+
+        this.model.on('change:publishedComments',
+                      this._onPublishedCommentsChanged, this);
+        this._onPublishedCommentsChanged();
+
+        /* Add any hooks. */
+        RB.CommentDialogHook.each(function(hook) {
+            var HookViewType = hook.get('viewType'),
+                hookView = new HookViewType({
+                    commentDialog: this,
+                    commentEditor: this.model,
+                    el: this.el
+                });
+
+            hookView.render();
+        }, this);
+
+        return this;
+    },
+
+    /*
+     * Callback for when the Save button is pressed.
+     *
+     * Saves the comment, creating it if it's new, and closes the dialog.
+     */
+    save: function() {
+        /*
+         * Set this immediately, in case new text has been set in the editor
+         * that we haven't been notified about yet.
+         */
+        this.model.set('text', this._textEditor.getText());
+
+        if (this.model.get('canSave')) {
+            this.model.save({
+                error: function(model, xhr) {
+                    alert(gettext('Error saving comment: ') + xhr.errorText);
+                }
+            }, this);
+            this.close();
+        }
+    },
+
+    /*
+     * Opens the comment dialog and focuses the text field.
+     */
+    open: function() {
+        function openDialog() {
+            this.$el.scrollIntoView();
+            this._textEditor.focus();
+        }
+
+        this.$el
+            .css({
+                top: parseInt(this.$el.css("top"), 10) - this.SLIDE_DISTANCE,
+                opacity: 0
+            })
+            .show();
+
+        this._handleResize();
+
+        if (this.model.get('canEdit')) {
+            this.model.beginEdit();
+        }
+
+        if (this.options.animate) {
+            this.$el.animate({
+                top: "+=" + this.SLIDE_DISTANCE + "px",
+                opacity: 1
+            }, 350, "swing", _.bind(openDialog, this));
+        } else {
+            openDialog.call(this);
+        }
+    },
+
+    /*
+     * Closes the comment dialog, discarding the comment block if empty.
+     *
+     * This can optionally take a callback and context to notify when the
+     * dialog has been closed.
+     */
+    close: function(onClosed, context) {
+        function closeDialog() {
+            this.model.close();
+            this.$el.remove();
+            this.trigger("closed");
+
+            if (_.isFunction(onClosed)) {
+                onClosed.call(context);
+            }
+        }
+
+        if (this.options.animate && this.$el.is(":visible")) {
+            this.$el.animate({
+                top: "-=" + this.SLIDE_DISTANCE + "px",
+                opacity: 0
+            }, 350, "swing", _.bind(closeDialog, this));
+        } else {
+            closeDialog.call(this);
+        }
+    },
+
+    /*
+     * Moves the comment dialog to the given coordinates.
+     */
+    move: function(x, y) {
+        this.$el.move(x, y);
+    },
+
+    /*
+     * Positions the dialog beside an element.
+     *
+     * This takes the same arguments that $.fn.positionToSide takes.
+     */
+    positionBeside: function($el, options) {
+        this.$el.positionToSide($el, options);
+    },
+
+    /*
+     * Updates the title of the comment dialog, based on the current state.
+     */
+    _updateTitle: function() {
+        this._$title.text(this.model.get('dirty')
+                          ? gettext('Your comment (unsaved)')
+                          : gettext('Your comment'));
+    },
+
+    /*
+     * Callback for when the list of published comments changes.
+     *
+     * Sets the list of comments in the CommentsList, and factors in some
+     * new layout properties.
+     */
+    _onPublishedCommentsChanged: function() {
+        var comments = this.model.get('publishedComments') || [],
+            showComments = (comments.length > 0),
+            width = this.FORM_BOX_WIDTH;
+
+        this.commentsList.setComments(comments,
+                                       this.model.get('publishedCommentsType'));
+        this._$commentsPane.setVisible(showComments);
+
+        /* Do this here so that calculations can be done before open() */
+
+        if (showComments) {
+            width += this.COMMENTS_BOX_WIDTH;
+        }
+
+        this.$el
+            .width(width)
+            .height(this.model.get('canEdit')
+                    ? this.DIALOG_TOTAL_HEIGHT
+                    : this.DIALOG_NON_EDITABLE_HEIGHT);
+    },
+
+    /*
+     * Handles the resize of the comment dialog. This will lay out the
+     * elements in the dialog appropriately.
+     */
+    _handleResize: function() {
+        var $draftForm = this._$draftForm,
+            $commentsPane = this._$commentsPane,
+            $commentsList = this.commentsList.$el,
+            $textField = this._textEditor.$el,
+            width = this.$el.width(),
+            height = this.$el.height(),
+            commentsWidth = 0;
+
+        if ($commentsPane.is(":visible")) {
+            $commentsPane
+                .outerWidth(this.COMMENTS_BOX_WIDTH)
+                .outerHeight(height)
+                .move(0, 0, "absolute");
+
+            $commentsList.height($commentsPane.height() -
+                                 $commentsList.position().top);
+
+            commentsWidth = $commentsPane.outerWidth(true);
+            width -= commentsWidth;
+        }
+
+        $draftForm
+            .outerWidth(width)
+            .outerHeight(height)
+            .move(commentsWidth, 0, "absolute");
+
+        this._textEditor.setSize(
+            (this._$body.width() -
+             $textField.getExtents('b', 'lr')),
+            ($draftForm.height() -
+             this._$header.outerHeight() -
+             this._$commentOptions.outerHeight() -
+             this._$footer.outerHeight() -
+             $textField.getExtents('b', 'tb')));
+    },
+
+    /*
+     * Callback for when the Cancel button is pressed.
+     *
+     * Cancels the comment (which may delete the comment block, if it's new)
+     * and closes the dialog.
+     */
+    _onCancelClicked: function() {
+        this.model.cancel();
+        this.close();
+    },
+
+    /*
+     * Callback for when the Delete button is pressed.
+     *
+     * Deletes the comment and closes the dialog.
+     */
+    _onDeleteClicked: function() {
+        if (this.model.get('canDelete')) {
+            this.model.deleteComment();
+            this.close();
+        }
+    },
+
+    /*
+     * Callback for key down events in the text field.
+     *
+     * If the Escape key is pressed, the dialog will be closed.
+     *
+     * The keydown event won't be propagated to the parent elements.
+     */
+    _onTextKeyDown: function(e) {
+        e.stopPropagation();
+
+        if (e.which === $.ui.keyCode.ESCAPE) {
+            this._onCancelClicked();
+            return false;
+        }
+    },
+
+    /*
+     * Callback for key press events in the text field.
+     *
+     * If the Control-Enter or Alt-I keys are pressed, we'll handle them
+     * specially. Control-enter is the same thing as clicking Save,
+     * and Alt-I is the same as toggling the Issue checkbox.
+     */
+    _onTextKeyPress: function(e) {
+        e.stopPropagation();
+
+        switch (e.which) {
+            case 10:
+            case $.ui.keyCode.ENTER:
+                /* Enter */
+                if (e.ctrlKey) {
+                    this.save();
+                    return false;
+                }
+                break;
+
+            case 73:
+            case 105:
+                /* I */
+                if (e.altKey) {
+                    this.model.set('openIssue', !this.model.get('openIssue'));
+                }
+                break;
+
+            case 77:
+            case 109:
+                /* M */
+                if (e.altKey) {
+                    this.model.set('richText', !this.model.get('richText'));
+                }
+                break;
+
+            default:
+                break;
+        }
+    }
+}, {
+    /*
+     * Add some useful singletons to CommentDialogView for managing
+     * comment dialogs.
+     */
+
+    _instance: null,
+
+    /*
+     * Creates and shows a new comment dialog and associated model.
+     *
+     * This is a class method that handles providing a comment dialog
+     * ready to use with the given state.
+     *
+     * Only one comment dialog can appear on the screen at any given time
+     * when using this.
+     */
+    create: function(options) {
+        var instance = RB.CommentDialogView._instance,
+            reviewRequestEditor =
+                options.reviewRequestEditor ||
+                RB.PageManager.getPage().reviewRequestEditor,
+            commentIssueManager =
+                options.commentIssueManager ||
+                reviewRequestEditor.get('commentIssueManager'),
+            beside,
+            dlg,
+            x,
+            y;
+
+        function showCommentDlg() {
+            try {
+                dlg.open();
+            } catch(e) {
+                dlg.close();
+                throw e;
+            }
+
+            RB.CommentDialogView._instance = dlg;
+        }
+
+        console.assert(options.comment, 'A comment must be specified');
+
+        options = options || {};
+
+        dlg = new RB.CommentDialogView({
+            animate: options.animate,
+            commentIssueManager: commentIssueManager,
+            model: new RB.CommentEditor({
+                comment: options.comment,
+                reviewRequest: reviewRequestEditor.get('reviewRequest'),
+                reviewRequestEditor: reviewRequestEditor,
+                publishedComments: options.publishedComments || undefined,
+                publishedCommentsType: options.publishedCommentsType ||
+                                       undefined
+            })
+        });
+
+        dlg.render().$el
+            .css('z-index', 999) // XXX Use classes for z-indexes.
+            .appendTo(options.container || document.body);
+
+        options.position = options.position || {};
+
+        if (_.isFunction(options.position)) {
+            options.position(dlg);
+        } else if (options.position.beside) {
+            beside = options.position.beside;
+            dlg.positionBeside(beside.el, beside);
+        } else {
+            x = options.position.x;
+            y = options.position.y;
+
+            if (x === undefined) {
+                /* Center it. */
+                x = $(document).scrollLeft() +
+                    ($(window).width() - dlg.$el.width()) / 2;
+            }
+
+            if (y === undefined) {
+                /* Center it. */
+                y = $(document).scrollTop() +
+                    ($(window).height() - dlg.$el.height()) / 2;
+            }
+
+            dlg.move(x, y);
+        }
+
+        dlg.on('closed', function() {
+            RB.CommentDialogView._instance = null;
+        });
+
+        if (instance) {
+            instance.on('closed', showCommentDlg);
+            instance.close();
+        } else {
+            showCommentDlg();
+        }
+
+        return dlg;
+    }
+});
--- a/reviewboard/reviewboard/staticbundles.py
+++ b/reviewboard/reviewboard/staticbundles.py
@@ -203,17 +203,17 @@ PIPELINE_JS = dict({
             'rb/js/pages/models/diffViewerPageModel.js',
             'rb/js/pages/views/reviewablePageView_mozreview.js',
             'rb/js/pages/views/reviewRequestPageView.js',
             'rb/js/pages/views/diffViewerPageView.js',
             'rb/js/utils/textUtils.js',
             'rb/js/views/abstractCommentBlockView.js',
             'rb/js/views/abstractReviewableView.js',
             'rb/js/views/collapsableBoxView.js',
-            'rb/js/views/commentDialogView.js',
+            'rb/js/views/commentDialogView_mozreview.js',
             'rb/js/views/commentIssueBarView.js',
             'rb/js/views/diffFragmentQueueView.js',
             'rb/js/views/dndUploaderView.js',
             'rb/js/views/draftReviewBannerView_mozreview.js',
             'rb/js/views/uploadAttachmentView.js',
             'rb/js/views/revisionSelectorView.js',
             'rb/js/views/fileAttachmentCommentBlockView.js',
             'rb/js/views/fileAttachmentReviewableView.js',