Commit ff23636c authored by Fatih Acet's avatar Fatih Acet 🔥

Merge branch '23696-fix-diff-view-highlighting' into 'master'

Resolve "Highlighting lines is broken"

## What does this MR do?

Add line highlighting back to diff view.  This was working in the MR "changes" tab, but not on a commit page such as winniehell/reproduction-area@9101e66f

~~This MR also fixes the `scrollToElement` method in `MergeRequestTabs` to account for the extra height of the tab links which are now fixed in place once they are scrolled to the top of the screen.~~ (removed in favor of !7051)

This MR also refactors much of the `Diff` and `MergeRequestTabs` classes to es6 syntax in an effort to increase readability.

## Are there points in the code the reviewer needs to double check?

Check out both MR "change" tabs and commit diff pages and ensure that line highlighting works and that loading a page with one of these permalink hashes correctly highlights and scrolls to the line.

Ensure I didn't break anything in the transition to es6 syntax.  Check the functionality of the tabs on MR pages, as well as diff page interactivity (unfolding hidden lines in diff files, adding comments to diffs, etc).  I have checked these myself, but another set of eyes would be a good idea.

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md) entry added
- Tests
  - [x] All builds are passing
- [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if it does - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)

## What are the relevant issue numbers?

Closes #23696

See merge request !7090
parents 668deeae ed5f22cb
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, max-len, one-var, camelcase, one-var-declaration-per-line, no-unused-vars, no-unused-expressions, no-sequences, object-shorthand, comma-dangle, prefer-arrow-callback, semi, radix, padded-blocks, max-len */
(function() {
this.Diff = (function() {
var UNFOLD_COUNT;
UNFOLD_COUNT = 20;
function Diff() {
$('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) {
return function(event) {
var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom;
target = $(event.target);
unfoldBottom = target.hasClass('js-unfold-bottom');
unfold = true;
ref = _this.lineNumbers(target.parent()), old_line = ref[0], line_number = ref[1];
offset = line_number - old_line;
if (unfoldBottom) {
line_number += 1;
since = line_number;
to = line_number + UNFOLD_COUNT;
} else {
ref1 = _this.lineNumbers(target.parent().prev()), prev_old_line = ref1[0], prev_new_line = ref1[1];
line_number -= 1;
to = line_number;
if (line_number - UNFOLD_COUNT > prev_new_line + 1) {
since = line_number - UNFOLD_COUNT;
} else {
since = prev_new_line + 1;
unfold = false;
}
}
file = target.parents('.diff-file');
link = file.data('blob-diff-path');
params = {
since: since,
to: to,
bottom: unfoldBottom,
offset: offset,
unfold: unfold,
view: file.data('view')
};
return $.get(link, params, function(response) {
return target.parent().replaceWith(response);
});
};
})(this));
}
Diff.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) {
return [0, 0];
}
return line.find('.diff-line-num').map(function() {
return parseInt($(this).data('linenumber'));
});
};
return Diff;
})();
}).call(this);
/* eslint-disable class-methods-use-this */
(() => {
const UNFOLD_COUNT = 20;
class Diff {
constructor() {
$('.files .diff-file').singleFileDiff();
$('.files .diff-file').filesCommentButton();
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
$(document)
.off('click', '.js-unfold, .diff-line-num a')
.on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
this.highlighSelectedLine();
}
handleClickUnfold(e) {
const $target = $(e.target);
// current babel config relies on iterators implementation, so we cannot simply do:
// const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent());
const ref = this.lineNumbers($target.parent());
const oldLineNumber = ref[0];
const newLineNumber = ref[1];
const offset = newLineNumber - oldLineNumber;
const bottom = $target.hasClass('js-unfold-bottom');
let since;
let to;
let unfold = true;
if (bottom) {
const lineNumber = newLineNumber + 1;
since = lineNumber;
to = lineNumber + UNFOLD_COUNT;
} else {
const lineNumber = newLineNumber - 1;
since = lineNumber - UNFOLD_COUNT;
to = lineNumber;
// make sure we aren't loading more than we need
const prevNewLine = this.lineNumbers($target.parent().prev())[1];
if (since <= prevNewLine + 1) {
since = prevNewLine + 1;
unfold = false;
}
}
const file = $target.parents('.diff-file');
const link = file.data('blob-diff-path');
const view = file.data('view');
const params = { since, to, bottom, offset, unfold, view };
$.get(link, params, response => $target.parent().replaceWith(response));
}
openAnchoredDiff(anchoredDiff, cb) {
const diffTitle = $(`#file-path-${anchoredDiff}`);
const diffFile = diffTitle.closest('.diff-file');
const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) {
diffFile.singleFileDiff(true, cb);
} else {
cb();
}
}
handleClickLineNum(e) {
const hash = $(e.currentTarget).attr('href');
e.preventDefault();
if (window.history.pushState) {
window.history.pushState(null, null, hash);
} else {
window.location.hash = hash;
}
this.highlighSelectedLine();
}
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
lineNumbers(line) {
if (!line.children().length) {
return [0, 0];
}
return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10));
}
highlighSelectedLine() {
const $diffFiles = $('.diff-file');
$diffFiles.find('.hll').removeClass('hll');
if (window.location.hash !== '') {
const hash = window.location.hash.replace('#', '');
$diffFiles
.find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`)
.addClass('hll');
}
}
}
window.gl = window.gl || {};
window.gl.Diff = Diff;
})();
......@@ -61,7 +61,7 @@
new ZenMode();
break;
case 'projects:compare:show':
new Diff();
new gl.Diff();
break;
case 'projects:issues:new':
case 'projects:issues:edit':
......@@ -74,7 +74,7 @@
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
new Diff();
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
......@@ -91,7 +91,7 @@
new GLForm($('.release-form'));
break;
case 'projects:merge_requests:show':
new Diff();
new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
new MergedButtons();
......@@ -101,7 +101,7 @@
new MergedButtons();
break;
case "projects:merge_requests:diffs":
new Diff();
new gl.Diff();
new ZenMode();
new MergedButtons();
break;
......@@ -117,7 +117,7 @@
break;
case 'projects:commit:show':
new Commit();
new Diff();
new gl.Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
......
......@@ -40,7 +40,7 @@
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
window.mrTabs = new MergeRequestTabs(this.opts);
window.mrTabs = new gl.MergeRequestTabs(this.opts);
};
MergeRequest.prototype.showAllCommits = function() {
......
/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Cookies */
/* global DiffNotesApp */
/* global Flash */
/*= require js.cookie */
/*= require breakpoints */
/* eslint-disable max-len */
// MergeRequestTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the MergeRequests#show page.
//
/*= require js.cookie */
//
// ### Example Markup
//
......@@ -45,70 +51,73 @@
// </div>
// </div>
//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.MergeRequestTabs = (function() {
MergeRequestTabs.prototype.diffsLoaded = false;
/* eslint-enable max-len */
MergeRequestTabs.prototype.buildsLoaded = false;
(() => {
// Store the `location` object, allowing for easier stubbing in tests
let location = window.location;
MergeRequestTabs.prototype.pipelinesLoaded = false;
class MergeRequestTabs {
MergeRequestTabs.prototype.commitsLoaded = false;
constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) {
this.diffsLoaded = false;
this.buildsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
MergeRequestTabs.prototype.fixedLayoutPref = null;
this.setUrl = setUrl !== undefined ? setUrl : true;
this.buildsLoaded = buildsLoaded || false;
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
this.buildsLoaded = this.opts.buildsLoaded || false;
if (stubLocation) {
location = stubLocation;
}
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
// Store the `location` object, allowing for easier stubbing in tests
this._location = location;
this.bindEvents();
this.activateTab(this.opts.action);
this.activateTab(action);
this.initAffix();
}
MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
$(document).on('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.unbindEvents = function() {
$(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
$(document).off('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.showTab = function(event) {
event.preventDefault();
return this.activateTab($(event.target).data('action'));
};
MergeRequestTabs.prototype.tabShown = function(event) {
var $target, action, navBarHeight;
$target = $(event.target);
action = $target.data('action');
bindEvents() {
$(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
}
unbindEvents() {
$(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
}
showTab(e) {
e.preventDefault();
this.activateTab($(e.target).data('action'));
}
tabShown(e) {
const $target = $(e.target);
const action = $target.data('action');
if (action === 'commits') {
this.loadCommits($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
if (Breakpoints.get().getBreakpointSize() !== 'lg') {
this.shrinkView();
}
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight
const navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo('.merge-request-details .merge-request-tabs', {
offset: -navBarHeight,
});
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
......@@ -122,32 +131,31 @@
this.expandView();
this.resetViewContainer();
}
if (this.opts.setUrl) {
if (this.setUrl) {
this.setCurrentAction(action);
}
};
}
MergeRequestTabs.prototype.scrollToElement = function(container) {
var $el, navBarHeight;
if (window.location.hash) {
navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight;
$el = $(container + " " + window.location.hash + ":not(.match)");
scrollToElement(container) {
if (location.hash) {
const offset = 0 - (
$('.navbar-gitlab').outerHeight() +
$('.layout-nav').outerHeight() +
$('.js-tabs-affix').outerHeight()
);
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
return $.scrollTo(container + " " + window.location.hash + ":not(.match)", {
offset: -navBarHeight
});
$.scrollTo($el[0], { offset });
}
}
};
}
// Activate a tab based on the current action
MergeRequestTabs.prototype.activateTab = function(action) {
if (action === 'show') {
action = 'notes';
}
activateTab(action) {
const activate = action === 'show' ? 'notes' : action;
// important note: the .tab('show') method triggers 'shown.bs.tab' event itself
$(".merge-request-tabs a[data-action='" + action + "']").tab('show');
};
$(`.merge-request-tabs a[data-action='${activate}']`).tab('show');
}
// Replaces the current Merge Request-specific action in the URL with a new one
//
......@@ -169,275 +177,214 @@
// location.pathname # => "/namespace/project/merge_requests/1/commits"
//
// Returns the new URL String
MergeRequestTabs.prototype.setCurrentAction = function(action) {
var new_state;
// Normalize action, just to be safe
if (action === 'show') {
action = 'notes';
}
this.currentAction = action;
setCurrentAction(action) {
this.currentAction = action === 'show' ? 'notes' : action;
// Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
if (this.currentAction !== 'notes') {
newState += `/${this.currentAction}`;
}
// Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
newState += location.search + location.hash;
// Replace the current history state with the new one without breaking
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
}, document.title, new_state);
return new_state;
};
window.history.replaceState({
turbolinks: true,
url: newState,
}, document.title, newState);
MergeRequestTabs.prototype.loadCommits = function(source) {
return newState;
}
loadCommits(source) {
if (this.commitsLoaded) {
return;
}
return this._get({
url: source + ".json",
success: (function(_this) {
return function(data) {
document.querySelector("div#commits").innerHTML = data.html;
gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
_this.commitsLoaded = true;
return _this.scrollToElement("#commits");
};
})(this)
this.ajaxGet({
url: `${source}.json`,
success: (data) => {
document.querySelector('div#commits').innerHTML = data.html;
gl.utils.localTimeAgo($('.js-timeago', 'div#commits'));
this.commitsLoaded = true;
this.scrollToElement('#commits');
},
});
};
}
MergeRequestTabs.prototype.loadDiff = function(source) {
loadDiff(source) {
if (this.diffsLoaded) {
return;
}
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
var url = document.createElement('a');
const url = document.createElement('a');
url.href = source;
return this._get({
url: (url.pathname + ".json") + this._location.search,
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
var anchoredDiff = gl.utils.getLocationHash();
if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() {
_this.scrollToElement("#diffs");
_this.highlighSelectedLine();
});
_this.filesCommentButton = $('.files .diff-file').filesCommentButton();
return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) {
e.preventDefault();
window.location.hash = $(e.currentTarget).attr('href');
_this.highlighSelectedLine();
return _this.scrollToElement("#diffs");
});
};
})(this)
this.ajaxGet({
url: `${url.pathname}.json${location.search}`,
success: (data) => {
$('#diffs').html(data.html);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
this.expandViewContainer();
}
this.diffsLoaded = true;
const diffPage = new gl.Diff();
const locationHash = gl.utils.getLocationHash();
const anchoredDiff = locationHash && locationHash.split('_')[0];
if (anchoredDiff) {
diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs'));
}
},
});
};
MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) {
var diffTitle = $('#file-path-' + anchoredDiff);
var diffFile = diffTitle.closest('.diff-file');
var nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) {
diffFile.singleFileDiff(true, cb);
} else {
cb();
}
};
MergeRequestTabs.prototype.highlighSelectedLine = function() {
var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
$('.hll').removeClass('hll');
locationHash = window.location.hash;
if (locationHash !== '') {
dataLineString = '[data-line-code="' + locationHash.replace('#', '') + '"]';
$diffLine = $(locationHash + ":not(.match)", $('#diffs'));
if (!$diffLine.is('tr')) {
$diffLine = $('#diffs').find("td" + locationHash + ", td" + dataLineString);
} else {
$diffLine = $diffLine.find('td');
}
if ($diffLine.length) {
$diffLine.addClass('hll');
diffLineTop = $diffLine.offset().top;
return navBarHeight = $('.navbar-gitlab').outerHeight();
}
}
};
}
MergeRequestTabs.prototype.loadBuilds = function(source) {
loadBuilds(source) {
if (this.buildsLoaded) {
return;
}
return this._get({
url: source + ".json",
success: (function(_this) {
return function(data) {
document.querySelector("div#builds").innerHTML = data.html;
gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
_this.buildsLoaded = true;
if (!this.pipelines) this.pipelines = new gl.Pipelines();
return _this.scrollToElement("#builds");
};
})(this)
this.ajaxGet({
url: `${source}.json`,
success: (data) => {
document.querySelector('div#builds').innerHTML = data.html;
gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
this.buildsLoaded = true;
new gl.Pipelines();
this.scrollToElement('#builds');
},
});
};
}
MergeRequestTabs.prototype.loadPipelines = function(source) {
loadPipelines(source) {
if (this.pipelinesLoaded) {
return;
}
return this._get({
url: source + ".json",
success: function(data) {
this.ajaxGet({
url: `${source}.json`,
success: (data) => {
$('#pipelines').html(data.html);
gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
this.pipelinesLoaded = true;
return this.scrollToElement("#pipelines");
}.bind(this)
this.scrollToElement('#pipelines');
},
});
};
}
// Show or hide the loading spinner
//
// status - Boolean, true to show, false to hide
MergeRequestTabs.prototype.toggleLoading = function(status) {
return $('.mr-loading-status .loading').toggle(status);
};
MergeRequestTabs.prototype._get = function(options) {
var defaults;
defaults = {
beforeSend: (function(_this) {
return function() {
return _this.toggleLoading(true);
};
})(this),
complete: (function(_this) {
return function() {
return _this.toggleLoading(false);
};
})(this),
toggleLoading(status) {
$('.mr-loading-status .loading').toggle(status);
}
ajaxGet(options) {
const defaults = {
beforeSend: () => this.toggleLoading(true),
error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET'
type: 'GET',
};
options = $.extend({}, defaults, options);
return $.ajax(options);
};
$.ajax($.extend({}, defaults, options));
}
MergeRequestTabs.prototype.diffViewType = function() {
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
};
}
MergeRequestTabs.prototype.isDiffAction = function(action) {
return action === 'diffs' || action === 'new/diffs'
};
isDiffAction(action) {
return action === 'diffs' || action === 'new/diffs';
}
MergeRequestTabs.prototype.expandViewContainer = function() {
var $wrapper = $('.content-wrapper .container-fluid');
expandViewContainer() {
const $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
};
}
MergeRequestTabs.prototype.resetViewContainer = function() {
resetViewContainer() {