clone for mozreview modification (bug 1115707); r?smacleod draft
authorbyron jones <glob@mozilla.com>
Mon, 19 Sep 2016 14:58:14 +0800
changeset 173 fdbfa60db88ee4eedb77d66788759cf4778651e2
parent 172 44d3d7eb2f3ed329e574fead70480e42b8cd35ae
child 174 a013fafced7729fc6e29d767f75082f7aa321bc2
push idunknown
push userunknown
push dateunknown
reviewerssmacleod
bugs1115707
clone for mozreview modification (bug 1115707); r?smacleod MozReview-Commit-ID: 106lpYGKf8I
reviewboard/reviewboard/static/rb/js/diffviewer/views/diffReviewableView_mozreview.js
reviewboard/reviewboard/staticbundles.py
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/diffviewer/views/diffReviewableView_mozreview.js
@@ -0,0 +1,641 @@
+(function() {
+
+
+/*
+ * Handles reviews of the diff for a file.
+ *
+ * This provides commenting abilities for ranges of lines on a diff, as well
+ * as showing existing comments, and handling other interaction around
+ * per-file diffs.
+ */
+RB.DiffReviewableView = RB.AbstractReviewableView.extend({
+    tagName: 'table',
+
+    commentBlockView: RB.DiffCommentBlockView,
+    commentsListName: 'diff_comments',
+
+    cssTemplate: _.template([
+        '#<%= id %> td pre,',
+        '#<%= id %> .revision-row th.revision-col {',
+        '    min-width: <%= minColWidth %>px;',
+        '    max-width: <%= maxColWidth %>px;',
+        '}',
+        '#<%= id %> .filename-row th {',
+        '    min-width: <%= minFilenameWidth %>px;',
+        '    max-width: <%= maxFilenameWidth %>px;',
+        '}'
+    ].join('\n')),
+
+    events: {
+        'click .download-link': '_onDownloadLinkClicked',
+        'click thead tr': '_onFileHeaderClicked',
+        'click .moved-to, .moved-from': '_onMovedLineClicked',
+        'click .diff-collapse-btn': '_onCollapseChunkClicked',
+        'click .diff-expand-btn': '_onExpandChunkClicked',
+        'mouseup': '_onMouseUp'
+    },
+
+    /*
+     * Initializes the reviewable for a file's diff.
+     */
+    initialize: function() {
+        _super(this).initialize.call(this);
+
+        _.bindAll(this, '_updateCollapseButtonPos', '_onWindowResize');
+
+        this._selector = new RB.TextCommentRowSelector({
+            el: this.el,
+            reviewableView: this
+        });
+
+        this._hiddenCommentBlockViews = [];
+        this._visibleCommentBlockViews = [];
+        this._$collapseButtons = $();
+
+        /* State for keeping consistent column widths for diff content. */
+        this._$filenameRow = null;
+        this._$revisionRow = null;
+        this._$css = null;
+        this._filenameReservedWidths = 0;
+        this._colReservedWidths = 0;
+        this._numColumns = 0;
+        this._numFilenameColumns = 0;
+        this._prevContentWidth = null;
+
+        /*
+         * Wrap this only once so we don't have to re-wrap every time
+         * the page scrolls.
+         */
+        this._$window = $(window);
+        this._$parent = this.$el.parent();
+
+        this.on('commentBlockViewAdded', this._placeCommentBlockView, this);
+    },
+
+    /*
+     * Removes the reviewable from the DOM.
+     */
+    remove: function() {
+        RB.AbstractReviewableView.prototype.remove.call(this);
+
+        this._$window.off('scroll', this._updateCollapseButtonPos);
+        this._$window.off('resize', this._onWindowResize);
+
+        this._selector.remove();
+    },
+
+    /*
+     * Renders the reviewable.
+     */
+    render: function() {
+        var $thead;
+
+        _super(this).render.call(this);
+
+        $thead = this.$('thead');
+
+        this._$revisionRow = $thead.find('.revision-row');
+        this._$filenameRow = $thead.find('.filename-row');
+        this._$css = $('<style/>').appendTo(this.$el);
+
+        this._selector.render();
+
+        _.each(this.$el.children('tbody.binary'), function(thumbnailEl) {
+            var $thumbnail = $(thumbnailEl),
+                id = $thumbnail.data('file-id'),
+                $caption = $thumbnail.find('.file-caption .edit'),
+                reviewRequest = this.model.get('reviewRequest'),
+                fileAttachment = reviewRequest.createFileAttachment({
+                    id: id
+                });
+
+            if (!$caption.hasClass('empty-caption')) {
+                fileAttachment.set('caption', $caption.text());
+            }
+        }, this);
+
+        this._precalculateContentWidths();
+        this._updateColumnSizes();
+
+        this._$window.on('scroll', this._updateCollapseButtonPos);
+        this._$window.on('resize', this._onWindowResize);
+
+        return this;
+    },
+
+    /*
+     * Toggles the display of whitespace-only chunks.
+     */
+    toggleWhitespaceOnlyChunks: function() {
+        this.$('tbody tr.whitespace-line').toggleClass('dimmed');
+
+        _.each(this.$el.children('tbody.whitespace-chunk'), function(chunk) {
+            var $chunk = $(chunk),
+                dimming = $chunk.hasClass('replace'),
+                chunkID = chunk.id.split('chunk')[1],
+                $children = $chunk.children();
+
+            $chunk.toggleClass('replace');
+
+            $($children[0]).toggleClass('first');
+            $($children[$children.length - 1]).toggleClass('last');
+
+            if (dimming) {
+                this.trigger('chunkDimmed', chunkID);
+            } else {
+                this.trigger('chunkUndimmed', chunkID);
+            }
+        }, this);
+
+        /*
+         * Swaps the visibility of the "This file has whitespace changes"
+         * tbody and the chunk siblings.
+         */
+        this.$el.children('tbody.whitespace-file')
+            .siblings('tbody')
+            .addBack()
+                .toggle();
+    },
+
+   /*
+    * Creates a comment for a chunk of a diff.
+    */
+    createComment: function(beginLineNum, endLineNum, beginNode, endNode) {
+        this._selector.createComment(beginLineNum, endLineNum, beginNode,
+                                     endNode);
+    },
+
+    /*
+     * Places a CommentBlockView on the page.
+     *
+     * This will compute the row range for the CommentBlockView and then
+     * render it to the screen, if the row range exists.
+     *
+     * If it doesn't exist yet, the CommentBlockView will be stored in the
+     * list of hidden comment blocks for later rendering.
+     */
+    _placeCommentBlockView: function(commentBlockView, prevBeginRowIndex) {
+        var commentBlock = commentBlockView.model,
+            rowEls = this._selector.getRowsForRange(
+                commentBlock.get('beginLineNum'),
+                commentBlock.get('endLineNum'),
+                prevBeginRowIndex),
+            beginRowEl,
+            endRowEl;
+
+        if (rowEls) {
+            beginRowEl = rowEls[0];
+            endRowEl = rowEls[1];
+
+            /*
+             * Note that endRow might be null if it exists in a collapsed
+             * region, so we can get away with just using beginRow if we
+             * need to.
+             */
+            commentBlockView.setRows($(beginRowEl), $(endRowEl || beginRowEl));
+            commentBlockView.$el.appendTo(
+                commentBlockView.$beginRow[0].cells[0]);
+            this._visibleCommentBlockViews.push(commentBlockView);
+
+            prevBeginRowIndex = beginRowEl.rowIndex;
+        } else {
+            this._hiddenCommentBlockViews.push(commentBlockView);
+        }
+
+        return prevBeginRowIndex;
+    },
+
+    /*
+     * Places any hidden comment blocks onto the diff viewer.
+     */
+    _placeHiddenCommentBlockViews: function() {
+        var hiddenCommentBlockViews = this._hiddenCommentBlockViews,
+            prevBeginRowIndex;
+
+        this._hiddenCommentBlockViews = [];
+
+        _.each(hiddenCommentBlockViews, function(commentBlockView) {
+            prevBeginRowIndex = this._placeCommentBlockView(commentBlockView,
+                                                            prevBeginRowIndex);
+        }, this);
+    },
+
+    _hideRemovedCommentBlockViews: function() {
+        var visibleCommentBlockViews = this._visibleCommentBlockViews;
+
+        this._visibleCommentBlockViews = [];
+
+        _.each(visibleCommentBlockViews, function(commentBlockView) {
+            if (commentBlockView.$el.is(':visible')) {
+                this._visibleCommentBlockViews.push(commentBlockView);
+            } else {
+                this._hiddenCommentBlockViews.push(commentBlockView);
+            }
+        }, this);
+
+        /*
+         * Sort these by line number so we can efficiently place them later.
+         */
+        _.sortBy(this._hiddenCommentBlockViews, function(commentBlockView) {
+            return commentBlockView.model.get('beginLineNum');
+        });
+    },
+
+    /*
+     * Update the positions of the collapse buttons.
+     *
+     * This will attempt to position the collapse buttons such that they're
+     * in the center of the exposed part of the expanded chunk in the current
+     * viewport.
+     *
+     * As the user scrolls, they'll be able to see the button scroll along
+     * with them. It will not, however, leave the confines of the expanded
+     * chunk.
+     */
+    _updateCollapseButtonPos: function() {
+        var windowTop,
+            windowHeight,
+            len = this._$collapseButtons.length,
+            $button,
+            $tbody,
+            parentOffset,
+            parentTop,
+            parentHeight,
+            i,
+            y1,
+            y2;
+
+        if (len === 0) {
+            return;
+        }
+
+        windowTop = this._$window.scrollTop();
+        windowHeight = this._$window.height();
+
+        for (i = 0; i < len; i++) {
+            $button = $(this._$collapseButtons[i]);
+            $tbody = $button.parents('tbody');
+            parentOffset = $tbody.offset();
+            parentTop = parentOffset.top;
+            parentHeight = $tbody.height();
+
+            /*
+             * We're going to first try to limit our processing to expanded
+             * chunks that are currently on the screen. We'll skip over any
+             * before those chunks, and stop once we're sure we have no further
+             * ones we can show.
+             */
+            if (parentTop >= windowTop + windowHeight) {
+                /* We hit the last one, so we're done. */
+                break;
+            } else if (parentTop + parentHeight <= windowTop) {
+                /* We're not there yet. */
+            } else {
+                /* Center the button in the view. */
+                if (   windowTop >= parentTop
+                    && windowTop + windowHeight <= parentTop + parentHeight) {
+                    if ($button.css('position') !== 'fixed') {
+                        /*
+                         * Position this fixed in the center of the screen.
+                         * It'll be less jumpy.
+                         */
+                        $button.css({
+                            position: 'fixed',
+                            left: $button.offset().left,
+                            top: Math.round((windowHeight -
+                                             $button.outerHeight()) / 2)
+                        });
+                    }
+
+                    /*
+                     * Since the expanded chunk is taking up the whole screen,
+                     * we have nothing else to process, so break.
+                     */
+                    break;
+                } else {
+                    y1 = Math.max(windowTop, parentTop);
+                    y2 = Math.min(windowTop + windowHeight,
+                                  parentTop + parentHeight);
+
+                    /*
+                     * The area doesn't take up the entire height of the
+                     * view. Switch back to an absolute position.
+                     */
+                    $button.css({
+                        position: 'absolute',
+                        left: '',
+                        top: y1 - parentTop +
+                             Math.round((y2 - y1 - $button.outerHeight()) / 2)
+                    });
+                }
+            }
+        }
+    },
+
+    /*
+     * Expands or collapses a chunk in a diff.
+     *
+     * This is called internally when an expand or collapse button is pressed
+     * for a chunk. It will fetch the diff and render it, displaying any
+     * contained comments, and setting up the resulting expand or collapse
+     * buttons.
+     */
+    _expandOrCollapse: function($btn, expanding) {
+        var chunkIndex = $btn.data('chunk-index'),
+            linesOfContext = $btn.data('lines-of-context');
+
+        this.model.getRenderedDiffFragment({
+            chunkIndex: chunkIndex,
+            linesOfContext: linesOfContext
+        }, {
+            success: function(html) {
+                var $tbody = $btn.parents('tbody'),
+                    $scrollAnchor,
+                    tbodyID,
+                    scrollAnchorID,
+                    scrollOffsetTop,
+                    newEl;
+
+                /*
+                 * We want to position the new chunk or collapse button at
+                 * roughly the same position as the chunk or collapse button
+                 * that the user pressed. Figure out what it is exactly and what
+                 * the scroll offsets are so we can later reposition the scroll
+                 * offset.
+                 */
+                if (expanding) {
+                    $scrollAnchor = this.$el;
+                    scrollAnchorID = $scrollAnchor[0].id;
+
+                    if (linesOfContext === 0) {
+                        /*
+                         * We've expanded the entire chunk, so we'll be looking
+                         * for the collapse button.
+                         */
+                        tbodyID = /collapsed-(.*)/.exec(scrollAnchorID)[1];
+                    } else {
+                        tbodyID = scrollAnchorID;
+                    }
+                } else {
+                    $scrollAnchor = $btn;
+                }
+
+                scrollOffsetTop = $scrollAnchor.offset().top -
+                                  this._$window.scrollTop();
+
+                /*
+                 * If we already expanded, we may have one or two loaded chunks
+                 * adjacent to the header. We want to remove those, since we'll
+                 * be generating new ones that include that data.
+                 */
+                $tbody.prev('.diff-header, .loaded').remove();
+                $tbody.next('.diff-header, .loaded').remove();
+
+                /*
+                 * Replace the header with the new HTML. This may also include a
+                 * new header.
+                 */
+                $tbody.replaceWith(html);
+
+                if (expanding) {
+                    this._placeHiddenCommentBlockViews();
+                } else {
+                    this._hideRemovedCommentBlockViews();
+                }
+
+                /*
+                 * Get the new tbody for the header, if any, and try to center.
+                 */
+                if (tbodyID) {
+                    newEl = document.getElementById(tbodyID);
+
+                    if (newEl) {
+                        $scrollAnchor = $(newEl);
+
+                        if ($scrollAnchor.length > 0) {
+                            this._$window.scrollTop(
+                                $scrollAnchor.offset().top -
+                                scrollOffsetTop);
+                        }
+                    }
+                }
+
+                /* Recompute the list of buttons for later use. */
+                this._$collapseButtons = this.$('.diff-collapse-btn');
+                this._updateCollapseButtonPos();
+
+                /*
+                 * We'll need to update the column sizes, but first, we need
+                 * to re-calculate things like the line widths, since they
+                 * may be longer after expanding.
+                 */
+                this._precalculateContentWidths();
+                this._updateColumnSizes();
+
+                this.trigger('chunkExpansionChanged');
+            }
+        }, this);
+    },
+
+    /*
+     * Pre-calculate the widths and other state needed for column widths.
+     *
+     * This will store the number of columns and the reserved space that
+     * needs to be subtracted from the container width, to be used in later
+     * calculating the desired widths of the content areas.
+     */
+    _precalculateContentWidths: function() {
+        var $cells,
+            containerExtents,
+            cellPadding;
+
+        if (!this.$el.hasClass('diff-error') && this._$revisionRow.length > 0) {
+            containerExtents = this.$el.getExtents('p', 'lr');
+
+            /* Calculate the widths and state of the diff columns. */
+            $cells = $(this._$revisionRow[0].cells);
+            cellPadding = this.$('pre:first').parent().andSelf()
+                .getExtents('p', 'lr');
+
+            this._colReservedWidths = $cells.eq(0).outerWidth() + cellPadding +
+                                      containerExtents;
+            this._numColumns = $cells.length;
+
+            if (this._numColumns === 4) {
+                /* There's a left-hand side and a right-hand side. */
+                this._colReservedWidths += $cells.eq(2).outerWidth() +
+                                           cellPadding;
+            }
+
+            /* Calculate the widths and state of the filename columns. */
+            $cells = $(this._$filenameRow[0].cells);
+            cellPadding = $cells.eq(0).getExtents('p', 'lr');
+            this._numFilenameColumns = $cells.length;
+            this._filenameReservedWidths = containerExtents +
+                                           2 * this._numFilenameColumns;
+        } else {
+            this._colReservedWidths = 0;
+            this._filenameReservedWidths = 0;
+            this._numColumns = 0;
+            this._numFilenameColumns = 0;
+        }
+    },
+
+    /*
+     * Update the sizes of the diff content columns.
+     *
+     * This will figure out the minimum and maximum widths of the columns
+     * and set them in a stylesheet, ensuring that lines will constrain to
+     * those sizes (force-wrapping if necessary) without overflowing or
+     * causing the other column to shrink too small.
+     */
+    _updateColumnSizes: function() {
+        var fullWidth,
+            contentWidth,
+            filenameWidth;
+
+        if (this.$el.hasClass('diff-error')) {
+            return;
+        }
+
+        fullWidth = this._$parent.width();
+
+        /* Calculate the desired widths of the diff columns. */
+        contentWidth = fullWidth - this._colReservedWidths;
+
+        if (this._numColumns === 4) {
+            contentWidth /= 2;
+        }
+
+        /* Calculate the desired widths of the filename columns. */
+        filenameWidth = fullWidth - this._filenameReservedWidths;
+
+        if (this._numFilenameColumns === 2) {
+            filenameWidth /= 2;
+        }
+
+        if (contentWidth !== this._prevContentWidth ||
+            filenameWidth !== this._prevFilenameWidth) {
+            /* The widths have changed, so force new minimums and maximums. */
+            this._$css.html(this.cssTemplate({
+                id: this.el.id,
+                minColWidth: Math.ceil(contentWidth * 0.66),
+                maxColWidth: Math.ceil(contentWidth),
+                minFilenameWidth: Math.ceil(filenameWidth * 0.66),
+                maxFilenameWidth: Math.ceil(filenameWidth)
+            }));
+
+            this._prevContentWidth = contentWidth;
+            this._prevFilenameWidth = filenameWidth;
+        }
+    },
+
+    /*
+     * Handler for when the window resizes.
+     *
+     * Updates the sizes of the diff columns, and the location of the
+     * collapse buttons (if one or more are visible).
+     */
+    _onWindowResize: function() {
+        this._updateColumnSizes();
+        this._updateCollapseButtonPos();
+    },
+
+    /*
+     * Handler for when a file download link is clicked.
+     *
+     * Prevents the event from bubbling up and being caught by
+     * _onFileHeaderClicked.
+     */
+    _onDownloadLinkClicked: function(e) {
+        e.stopPropagation();
+    },
+
+    /*
+     * Handler for when the file header is clicked.
+     *
+     * Highlights the file header.
+     */
+    _onFileHeaderClicked: function() {
+        this.trigger('fileClicked');
+
+        return false;
+    },
+
+    /*
+     * Handler for clicks on a "Moved to/from" flag.
+     *
+     * This will scroll to the location on the other end of the move,
+     * and briefly highlight the line.
+     */
+    _onMovedLineClicked: function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        this.trigger('moveFlagClicked', $(e.target).data('line'));
+    },
+
+    /*
+     * Handles the mouse up event.
+     *
+     * This will select any chunk that was clicked, highlight the chunk,
+     * and ensure it's cleanly scrolled into view.
+     */
+    _onMouseUp: function(e) {
+        var node = e.target,
+            $tbody;
+
+        /*
+         * The user clicked somewhere else. Move the anchor point here
+         * if it's part of the diff.
+         */
+        $tbody = $(node).parents('tbody:first');
+
+        if ($tbody.length > 0 &&
+            ($tbody.hasClass('delete') ||
+             $tbody.hasClass('insert') ||
+             $tbody.hasClass('replace'))) {
+            this.trigger('chunkClicked', $tbody.find('a:first').attr('name'));
+        }
+    },
+
+    /*
+     * Handler for Expand buttons.
+     *
+     * The Expand buttons will expand a collapsed chunk, either entirely
+     * or by certain amounts. It will fetch the new chunk contents and
+     * inject it into the diff viewer.
+     */
+    _onExpandChunkClicked: function(e) {
+        var $target = $(e.target);
+
+        if (!$target.hasClass('diff-expand-btn')) {
+            /* We clicked an image inside the link. Find the parent. */
+            $target = $target.parents('.diff-expand-btn');
+        }
+
+        e.preventDefault();
+        this._expandOrCollapse($target, true);
+    },
+
+    /*
+     * Handler for the Collapse button.
+     *
+     * The fully collapsed representation of that chunk will be fetched
+     * and put into the diff viewer in place of the expanded chunk.
+     */
+    _onCollapseChunkClicked: function(e) {
+        var $target = $(e.target);
+
+        if (!$target.hasClass('diff-collapse-btn')) {
+            /* We clicked an image inside the link. Find the parent. */
+            $target = $target.parents('.diff-collapse-btn');
+        }
+
+        e.preventDefault();
+        this._expandOrCollapse($target, false);
+    }
+});
+
+
+})();
--- a/reviewboard/reviewboard/staticbundles.py
+++ b/reviewboard/reviewboard/staticbundles.py
@@ -247,17 +247,17 @@ PIPELINE_JS = dict({
             'rb/js/diffviewer/models/diffRevisionModel.js',
             'rb/js/diffviewer/models/paginationModel.js',
             'rb/js/diffviewer/collections/diffFileCollection.js',
             'rb/js/diffviewer/views/chunkHighlighterView.js',
             'rb/js/diffviewer/views/diffCommentBlockView.js',
             'rb/js/diffviewer/views/diffCommentsHintView.js',
             'rb/js/diffviewer/views/diffComplexityIconView.js',
             'rb/js/diffviewer/views/diffFileIndexView.js',
-            'rb/js/diffviewer/views/diffReviewableView.js',
+            'rb/js/diffviewer/views/diffReviewableView_mozreview.js',
             'rb/js/diffviewer/views/diffRevisionLabelView.js',
             'rb/js/diffviewer/views/diffRevisionSelectorView.js',
             'rb/js/diffviewer/views/paginationView.js',
             'rb/js/diffviewer.js',
             'rb/js/reviews.js',
         ),
         'output_filename': 'rb/js/reviews.min.js',
     },