Commit a9441396 authored by Sam Bigelow's avatar Sam Bigelow

Scroll to diff file when clicking on file name

- Add an ID to the diff content
- handle clicking on file name in diffFileHeader when it is not a link
to another page but rather a link to an element on the page
parent 9bfdb1fa
...@@ -170,21 +170,23 @@ export default { ...@@ -170,21 +170,23 @@ export default {
</div> </div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" /> <gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<template v-else> <template v-else>
<div v-if="errorMessage" class="diff-viewer"> <div :id="`diff-content-${file.file_hash}`">
<div class="nothing-here-block" v-html="errorMessage"></div> <div v-if="errorMessage" class="diff-viewer">
<div class="nothing-here-block" v-html="errorMessage"></div>
</div>
<div v-else-if="isCollapsed" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }}
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
__('Click to expand it.')
}}</a>
</div>
<diff-content
v-else
:class="{ hidden: isCollapsed || isFileTooLarge }"
:diff-file="file"
:help-page-path="helpPagePath"
/>
</div> </div>
<div v-else-if="isCollapsed" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }}
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
__('Click to expand it.')
}}</a>
</div>
<diff-content
v-else
:class="{ hidden: isCollapsed || isFileTooLarge }"
:diff-file="file"
:help-page-path="helpPagePath"
/>
</template> </template>
<div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff"> <div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff">
{{ __('This source diff could not be displayed because it is too large.') }} {{ __('This source diff could not be displayed because it is too large.') }}
......
...@@ -11,6 +11,7 @@ import { __, s__, sprintf } from '~/locale'; ...@@ -11,6 +11,7 @@ import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue'; import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue'; import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
...@@ -66,6 +67,9 @@ export default { ...@@ -66,6 +67,9 @@ export default {
hasExpandedDiscussions() { hasExpandedDiscussions() {
return this.diffHasExpandedDiscussions(this.diffFile); return this.diffHasExpandedDiscussions(this.diffFile);
}, },
diffContentIDSelector() {
return `#diff-content-${this.diffFile.file_hash}`;
},
icon() { icon() {
if (this.diffFile.submodule) { if (this.diffFile.submodule) {
return 'archive'; return 'archive';
...@@ -77,6 +81,11 @@ export default { ...@@ -77,6 +81,11 @@ export default {
if (this.diffFile.submodule) { if (this.diffFile.submodule) {
return this.diffFile.submodule_tree_url || this.diffFile.submodule_link; return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
} }
if (!this.discussionPath) {
return this.diffContentIDSelector;
}
return this.discussionPath; return this.discussionPath;
}, },
filePath() { filePath() {
...@@ -152,6 +161,18 @@ export default { ...@@ -152,6 +161,18 @@ export default {
handleToggleDiscussions() { handleToggleDiscussions() {
this.toggleFileDiscussions(this.diffFile); this.toggleFileDiscussions(this.diffFile);
}, },
handleFileNameClick(e) {
const isLinkToOtherPage =
this.diffFile.submodule_tree_url || this.diffFile.submodule_link || this.discussionPath;
if (!isLinkToOtherPage) {
e.preventDefault();
const selector = this.diffContentIDSelector;
scrollToElement(document.querySelector(selector));
window.location.hash = selector;
}
},
}, },
}; };
</script> </script>
...@@ -171,7 +192,14 @@ export default { ...@@ -171,7 +192,14 @@ export default {
class="diff-toggle-caret append-right-5" class="diff-toggle-caret append-right-5"
@click.stop="handleToggle" @click.stop="handleToggle"
/> />
<a v-once ref="titleWrapper" :href="titleLink" class="append-right-4 js-title-wrapper"> <a
v-once
id="diffFile.file_path"
ref="titleWrapper"
class="append-right-4 js-title-wrapper"
:href="titleLink"
@click="handleFileNameClick"
>
<file-icon <file-icon
:file-name="filePath" :file-name="filePath"
:size="18" :size="18"
......
...@@ -7,6 +7,7 @@ import axios from './axios_utils'; ...@@ -7,6 +7,7 @@ import axios from './axios_utils';
import { getLocationHash } from './url_utility'; import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility'; import { convertToCamelCase } from './text_utility';
import { isObject } from './type_utility'; import { isObject } from './type_utility';
import BreakpointInstance from '../../breakpoints';
export const getPagePath = (index = 0) => { export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || ''; const page = $('body').attr('data-page') || '';
...@@ -193,16 +194,24 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; ...@@ -193,16 +194,24 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
export const contentTop = () => { export const contentTop = () => {
const perfBar = $('#js-peek').height() || 0; const perfBar = $('#js-peek').outerHeight() || 0;
const mrTabsHeight = $('.merge-request-tabs').height() || 0; const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0; const headerHeight = $('.navbar-gitlab').outerHeight() || 0;
const diffFilesChanged = $('.js-diff-files-changed').height() || 0; const diffFilesChanged = $('.js-diff-files-changed').outerHeight() || 0;
const diffFileLargeEnoughScreen = const mdScreenOrBigger = ['lg', 'md'].includes(BreakpointInstance.getBreakpointSize());
'matchMedia' in window ? window.matchMedia('min-width: 768') : true;
const diffFileTitleBar = const diffFileTitleBar =
(diffFileLargeEnoughScreen && $('.diff-file .file-title-flex-parent:visible').height()) || 0; (mdScreenOrBigger && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0;
const compareVersionsHeaderHeight =
(mdScreenOrBigger && $('.mr-version-controls').outerHeight()) || 0;
return perfBar + mrTabsHeight + headerHeight + diffFilesChanged + diffFileTitleBar; return (
perfBar +
mrTabsHeight +
headerHeight +
diffFilesChanged +
diffFileTitleBar +
compareVersionsHeaderHeight
);
}; };
export const scrollToElement = element => { export const scrollToElement = element => {
......
---
title: Scroll to diff file content when clicking on file header name and it is not
a link to other page
merge_request: !26422
author:
type: fixed
...@@ -3,7 +3,7 @@ import Vuex from 'vuex'; ...@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules'; import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules'; import notesModule from '~/notes/stores/modules';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import mountComponent, { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffDiscussionsMockData from '../mock_data/diff_discussions'; import diffDiscussionsMockData from '../mock_data/diff_discussions';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
...@@ -252,6 +252,75 @@ describe('diff_file_header', () => { ...@@ -252,6 +252,75 @@ describe('diff_file_header', () => {
expect(vm.$emit).not.toHaveBeenCalled(); expect(vm.$emit).not.toHaveBeenCalled();
}); });
}); });
describe('handleFileNameClick', () => {
let e;
beforeEach(() => {
e = { preventDefault: () => {} };
spyOn(e, 'preventDefault');
});
describe('when file name links to other page', () => {
it('does not call preventDefault if submodule tree url exists', () => {
vm = mountComponent(Component, {
...props,
diffFile: { ...props.diffFile, submodule_tree_url: 'foobar.com' },
});
vm.handleFileNameClick(e);
expect(e.preventDefault).not.toHaveBeenCalled();
});
it('does not call preventDefault if submodule_link exists', () => {
vm = mountComponent(Component, {
...props,
diffFile: { ...props.diffFile, submodule_link: 'foobar.com' },
});
vm.handleFileNameClick(e);
expect(e.preventDefault).not.toHaveBeenCalled();
});
it('does not call preventDefault if discussionPath exists', () => {
vm = mountComponent(Component, {
...props,
discussionPath: 'Foo bar',
});
vm.handleFileNameClick(e);
expect(e.preventDefault).not.toHaveBeenCalled();
});
});
describe('scrolling to diff', () => {
let scrollToElement;
let el;
beforeEach(() => {
el = document.createElement('div');
spyOn(document, 'querySelector').and.returnValue(el);
scrollToElement = spyOnDependency(DiffFileHeader, 'scrollToElement');
vm = mountComponent(Component, props);
vm.handleFileNameClick(e);
});
it('calls scrollToElement with file content', () => {
expect(scrollToElement).toHaveBeenCalledWith(el);
});
it('element adds the content id to the window location', () => {
expect(window.location.hash).toContain(props.diffFile.file_hash);
});
it('calls preventDefault when button does not link to other page', () => {
expect(e.preventDefault).toHaveBeenCalled();
});
});
});
}); });
describe('template', () => { describe('template', () => {
......
...@@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data';
import BreakpointInstance from '~/breakpoints';
const PIXEL_TOLERANCE = 0.2; const PIXEL_TOLERANCE = 0.2;
...@@ -380,6 +381,38 @@ describe('common_utils', () => { ...@@ -380,6 +381,38 @@ describe('common_utils', () => {
}); });
}); });
describe('contentTop', () => {
it('does not add height for fileTitle or compareVersionsHeader if screen is too small', () => {
spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('sm');
setFixtures(`
<div class="diff-file file-title-flex-parent">
blah blah blah
</div>
<div class="mr-version-controls">
more blah blah blah
</div>
`);
expect(commonUtils.contentTop()).toBe(0);
});
it('adds height for fileTitle and compareVersionsHeader screen is large enough', () => {
spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('lg');
setFixtures(`
<div class="diff-file file-title-flex-parent">
blah blah blah
</div>
<div class="mr-version-controls">
more blah blah blah
</div>
`);
expect(commonUtils.contentTop()).toBe(18);
});
});
describe('parseBoolean', () => { describe('parseBoolean', () => {
const { parseBoolean } = commonUtils; const { parseBoolean } = commonUtils;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment