/* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ /* global Flash */ import Cookies from 'js-cookie'; import CommitPipelinesTable from './commit/pipelines/pipelines_table'; import './breakpoints'; import './flash'; /* eslint-disable max-len */ // MergeRequestTabs // // Handles persisting and restoring the current tab selection and lazily-loading // content on the MergeRequests#show page. // // ### Example Markup // // // //
//
// Notes Content //
//
// Commits Content //
//
// Diffs Content //
//
// //
//
// Loading Animation //
//
// /* eslint-enable max-len */ (() => { // Store the `location` object, allowing for easier stubbing in tests let location = window.location; class MergeRequestTabs { constructor({ action, setUrl, stubLocation } = {}) { this.diffsLoaded = false; this.pipelinesLoaded = false; this.commitsLoaded = false; this.fixedLayoutPref = null; this.setUrl = setUrl !== undefined ? setUrl : true; this.setCurrentAction = this.setCurrentAction.bind(this); this.tabShown = this.tabShown.bind(this); this.showTab = this.showTab.bind(this); if (stubLocation) { location = stubLocation; } this.bindEvents(); this.activateTab(action); this.initAffix(); } bindEvents() { $(document) .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .on('click', '.js-show-tab', this.showTab); $('.merge-request-tabs a[data-toggle="tab"]') .on('click', this.clickTab); } unbindEvents() { $(document) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('click', '.js-show-tab', this.showTab); $('.merge-request-tabs a[data-toggle="tab"]') .off('click', this.clickTab); } destroyPipelinesView() { if (this.commitPipelinesTable) { document.querySelector('#commit-pipeline-table-view') .removeChild(this.commitPipelinesTable.$el); this.commitPipelinesTable.$destroy(); } } showTab(e) { e.preventDefault(); this.activateTab($(e.target).data('action')); } clickTab(e) { if (e.currentTarget && gl.utils.isMetaClick(e)) { const targetLink = e.currentTarget.getAttribute('href'); e.stopImmediatePropagation(); e.preventDefault(); window.open(targetLink, '_blank'); } } tabShown(e) { const $target = $(e.target); const action = $target.data('action'); if (action === 'commits') { this.loadCommits($target.attr('href')); this.expandView(); this.resetViewContainer(); this.destroyPipelinesView(); } else if (this.isDiffAction(action)) { this.loadDiff($target.attr('href')); if (Breakpoints.get().getBreakpointSize() !== 'lg') { this.shrinkView(); } if (this.diffViewType() === 'parallel') { this.expandViewContainer(); } this.destroyPipelinesView(); } else if (action === 'pipelines') { this.resetViewContainer(); this.mountPipelinesView(); } else { this.expandView(); this.resetViewContainer(); this.destroyPipelinesView(); } if (this.setUrl) { this.setCurrentAction(action); } } scrollToElement(container) { if (location.hash) { const offset = -$('.js-tabs-affix').outerHeight(); const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { offset }); } } } // Activate a tab based on the current action 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='${activate}']`).tab('show'); } // Replaces the current Merge Request-specific action in the URL with a new one // // If the action is "notes", the URL is reset to the standard // `MergeRequests#show` route. // // Examples: // // location.pathname # => "/namespace/project/merge_requests/1" // setCurrentAction('diffs') // location.pathname # => "/namespace/project/merge_requests/1/diffs" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" // setCurrentAction('notes') // location.pathname # => "/namespace/project/merge_requests/1" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" // setCurrentAction('commits') // location.pathname # => "/namespace/project/merge_requests/1/commits" // // Returns the new URL String setCurrentAction(action) { this.currentAction = action === 'show' ? 'notes' : action; // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' if (this.currentAction !== 'notes') { newState += `/${this.currentAction}`; } // Ensure parameters and hash come along for the ride newState += location.search + location.hash; // TODO: Consider refactoring in light of turbolinks removal. // Replace the current history state with the new one without breaking // Turbolinks' history. // // See https://github.com/rails/turbolinks/issues/363 window.history.replaceState({ url: newState, }, document.title, newState); return newState; } loadCommits(source) { if (this.commitsLoaded) { return; } 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'); }, }); } mountPipelinesView() { this.commitPipelinesTable = new CommitPipelinesTable().$mount(); // $mount(el) replaces the el with the new rendered component. We need it in order to mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount document.querySelector('#commit-pipeline-table-view') .appendChild(this.commitPipelinesTable.$el); } 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 const urlPathname = gl.utils.parseUrlPathname(source); this.ajaxGet({ url: `${urlPathname}.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; new gl.Diff(); this.scrollToElement('#diffs'); }, }); } // Show or hide the loading spinner // // status - Boolean, true to show, false to hide 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', }; $.ajax($.extend({}, defaults, options)); } diffViewType() { return $('.inline-parallel-buttons a.active').data('view-type'); } isDiffAction(action) { return action === 'diffs' || action === 'new/diffs'; } expandViewContainer() { const $wrapper = $('.content-wrapper .container-fluid'); if (this.fixedLayoutPref === null) { this.fixedLayoutPref = $wrapper.hasClass('container-limited'); } $wrapper.removeClass('container-limited'); } resetViewContainer() { if (this.fixedLayoutPref !== null) { $('.content-wrapper .container-fluid') .toggleClass('container-limited', this.fixedLayoutPref); } } shrinkView() { const $gutterIcon = $('.js-sidebar-toggle i:visible'); // Wait until listeners are set setTimeout(() => { // Only when sidebar is expanded if ($gutterIcon.is('.fa-angle-double-right')) { $gutterIcon.closest('a').trigger('click', [true]); } }, 0); } // Expand the issuable sidebar unless the user explicitly collapsed it expandView() { if (Cookies.get('collapsed_gutter') === 'true') { return; } const $gutterIcon = $('.js-sidebar-toggle i:visible'); // Wait until listeners are set setTimeout(() => { // Only when sidebar is collapsed if ($gutterIcon.is('.fa-angle-double-left')) { $gutterIcon.closest('a').trigger('click', [true]); } }, 0); } initAffix() { const $tabs = $('.js-tabs-affix'); // Screen space on small screens is usually very sparse // So we dont affix the tabs on these if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; const $diffTabs = $('#diff-notes-app'); $tabs.off('affix.bs.affix affix-top.bs.affix') .affix({ offset: { top: () => ( $diffTabs.offset().top - $tabs.height() ), }, }) .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); // Fix bug when reloading the page already scrolling if ($tabs.hasClass('affix')) { $tabs.trigger('affix.bs.affix'); } } } window.gl = window.gl || {}; window.gl.MergeRequestTabs = MergeRequestTabs; })();