2017-01-10 18:02:20 -05:00
|
|
|
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */
|
2016-07-24 16:45:11 -04:00
|
|
|
(function() {
|
|
|
|
(function(w) {
|
|
|
|
var base;
|
2017-04-06 06:40:39 -04:00
|
|
|
const faviconEl = document.getElementById('favicon');
|
|
|
|
const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
|
2016-07-24 16:45:11 -04:00
|
|
|
w.gl || (w.gl = {});
|
|
|
|
(base = w.gl).utils || (base.utils = {});
|
|
|
|
w.gl.utils.isInGroupsPage = function() {
|
|
|
|
return gl.utils.getPagePath() === 'groups';
|
|
|
|
};
|
|
|
|
w.gl.utils.isInProjectPage = function() {
|
|
|
|
return gl.utils.getPagePath() === 'projects';
|
|
|
|
};
|
|
|
|
w.gl.utils.getProjectSlug = function() {
|
|
|
|
if (this.isInProjectPage()) {
|
|
|
|
return $('body').data('project');
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
w.gl.utils.getGroupSlug = function() {
|
|
|
|
if (this.isInGroupsPage()) {
|
|
|
|
return $('body').data('group');
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
2016-09-05 23:28:59 -04:00
|
|
|
|
|
|
|
w.gl.utils.ajaxGet = function(url) {
|
|
|
|
return $.ajax({
|
|
|
|
type: "GET",
|
|
|
|
url: url,
|
|
|
|
dataType: "script"
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2017-05-05 06:57:29 -04:00
|
|
|
w.gl.utils.ajaxPost = function(url, data) {
|
|
|
|
return $.ajax({
|
|
|
|
type: 'POST',
|
|
|
|
url: url,
|
|
|
|
data: data,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-09-05 23:28:59 -04:00
|
|
|
w.gl.utils.extractLast = function(term) {
|
|
|
|
return this.split(term).pop();
|
|
|
|
};
|
|
|
|
|
|
|
|
w.gl.utils.rstrip = function rstrip(val) {
|
|
|
|
if (val) {
|
|
|
|
return val.replace(/\s+$/, '');
|
|
|
|
} else {
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-09-08 06:08:09 -04:00
|
|
|
gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
|
|
|
|
return $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
|
|
|
|
};
|
|
|
|
|
2016-09-05 23:28:59 -04:00
|
|
|
w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
|
|
|
|
event_name = event_name || 'input';
|
|
|
|
var closest_submit, field, that;
|
|
|
|
that = this;
|
|
|
|
field = $(field_selector);
|
|
|
|
closest_submit = field.closest('form').find(button_selector);
|
|
|
|
if (this.rstrip(field.val()) === "") {
|
|
|
|
closest_submit.disable();
|
|
|
|
}
|
|
|
|
return field.on(event_name, function() {
|
|
|
|
if (that.rstrip($(this).val()) === "") {
|
|
|
|
return closest_submit.disable();
|
|
|
|
} else {
|
|
|
|
return closest_submit.enable();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-11-21 13:18:49 -05:00
|
|
|
// automatically adjust scroll position for hash urls taking the height of the navbar into account
|
|
|
|
// https://github.com/twitter/bootstrap/issues/1768
|
|
|
|
w.gl.utils.handleLocationHash = function() {
|
2016-11-21 13:02:19 -05:00
|
|
|
var hash = w.gl.utils.getLocationHash();
|
|
|
|
if (!hash) return;
|
|
|
|
|
2017-02-09 14:22:38 -05:00
|
|
|
// This is required to handle non-unicode characters in hash
|
|
|
|
hash = decodeURIComponent(hash);
|
|
|
|
|
2017-07-28 07:11:05 -04:00
|
|
|
const fixedTabs = document.querySelector('.js-tabs-affix');
|
|
|
|
const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
|
|
|
|
const fixedNav = document.querySelector('.navbar-gitlab');
|
2017-06-23 04:24:15 -04:00
|
|
|
|
|
|
|
var adjustment = 0;
|
|
|
|
if (fixedNav) adjustment -= fixedNav.offsetHeight;
|
|
|
|
|
2016-11-21 13:02:19 -05:00
|
|
|
// scroll to user-generated markdown anchor if we cannot find a match
|
|
|
|
if (document.getElementById(hash) === null) {
|
2016-11-29 14:07:38 -05:00
|
|
|
var target = document.getElementById('user-content-' + hash);
|
2016-11-21 13:02:19 -05:00
|
|
|
if (target && target.scrollIntoView) {
|
|
|
|
target.scrollIntoView(true);
|
2017-06-23 04:24:15 -04:00
|
|
|
window.scrollBy(0, adjustment);
|
2016-11-21 13:02:19 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// only adjust for fixedTabs when not targeting user-generated content
|
|
|
|
if (fixedTabs) {
|
2017-06-23 04:24:15 -04:00
|
|
|
adjustment -= fixedTabs.offsetHeight;
|
2016-11-21 13:02:19 -05:00
|
|
|
}
|
2017-07-28 07:11:05 -04:00
|
|
|
|
|
|
|
if (fixedDiffStats) {
|
|
|
|
adjustment -= fixedDiffStats.offsetHeight;
|
|
|
|
}
|
|
|
|
|
2017-06-23 04:24:15 -04:00
|
|
|
window.scrollBy(0, adjustment);
|
2016-11-21 13:02:19 -05:00
|
|
|
}
|
2016-11-21 13:18:49 -05:00
|
|
|
};
|
2016-09-05 23:28:59 -04:00
|
|
|
|
2016-12-09 09:36:18 -05:00
|
|
|
// Check if element scrolled into viewport from above or below
|
|
|
|
// Courtesy http://stackoverflow.com/a/7557433/414749
|
|
|
|
w.gl.utils.isInViewport = function(el) {
|
|
|
|
var rect = el.getBoundingClientRect();
|
|
|
|
|
|
|
|
return (
|
|
|
|
rect.top >= 0 &&
|
|
|
|
rect.left >= 0 &&
|
|
|
|
rect.bottom <= window.innerHeight &&
|
|
|
|
rect.right <= window.innerWidth
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2016-12-06 17:11:09 -05:00
|
|
|
gl.utils.getPagePath = function(index) {
|
|
|
|
index = index || 0;
|
|
|
|
return $('body').data('page').split(':')[index];
|
2016-07-24 16:45:11 -04:00
|
|
|
};
|
2016-10-08 02:50:28 -04:00
|
|
|
|
2016-11-17 07:05:32 -05:00
|
|
|
gl.utils.parseUrl = function (url) {
|
|
|
|
var parser = document.createElement('a');
|
|
|
|
parser.href = url;
|
|
|
|
return parser;
|
|
|
|
};
|
|
|
|
|
|
|
|
gl.utils.parseUrlPathname = function (url) {
|
|
|
|
var parsedUrl = gl.utils.parseUrl(url);
|
|
|
|
// parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
|
|
|
|
// We have to make sure we always have an absolute path.
|
|
|
|
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
|
|
|
|
};
|
|
|
|
|
2016-12-12 22:24:55 -05:00
|
|
|
gl.utils.getUrlParamsArray = function () {
|
|
|
|
// We can trust that each param has one & since values containing & will be encoded
|
|
|
|
// Remove the first character of search as it is always ?
|
2017-05-15 09:03:52 -04:00
|
|
|
return window.location.search.slice(1).split('&').map((param) => {
|
|
|
|
const split = param.split('=');
|
|
|
|
return [decodeURI(split[0]), split[1]].join('=');
|
|
|
|
});
|
2016-12-13 09:52:43 -05:00
|
|
|
};
|
2016-12-12 22:24:55 -05:00
|
|
|
|
2016-10-08 02:50:28 -04:00
|
|
|
gl.utils.isMetaKey = function(e) {
|
|
|
|
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
|
|
|
|
};
|
|
|
|
|
2017-02-09 12:19:26 -05:00
|
|
|
gl.utils.isMetaClick = function(e) {
|
|
|
|
// Identify following special clicks
|
|
|
|
// 1) Cmd + Click on Mac (e.metaKey)
|
|
|
|
// 2) Ctrl + Click on PC (e.ctrlKey)
|
|
|
|
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
|
|
|
|
return e.metaKey || e.ctrlKey || e.which === 2;
|
|
|
|
};
|
|
|
|
|
2017-01-04 16:44:00 -05:00
|
|
|
gl.utils.scrollToElement = function($el) {
|
2016-12-29 11:19:11 -05:00
|
|
|
var top = $el.offset().top;
|
|
|
|
gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
|
|
|
|
|
|
|
|
return $('body, html').animate({
|
2017-02-07 18:20:28 -05:00
|
|
|
scrollTop: top - (gl.mrTabsHeight)
|
2016-12-29 11:19:11 -05:00
|
|
|
}, 200);
|
|
|
|
};
|
2016-07-24 16:45:11 -04:00
|
|
|
|
2017-01-09 12:26:43 -05:00
|
|
|
/**
|
|
|
|
this will take in the `name` of the param you want to parse in the url
|
|
|
|
if the name does not exist this function will return `null`
|
|
|
|
otherwise it will return the value of the param key provided
|
|
|
|
*/
|
2017-06-06 01:06:08 -04:00
|
|
|
w.gl.utils.getParameterByName = (name, parseUrl) => {
|
|
|
|
const url = parseUrl || window.location.href;
|
2017-01-09 12:26:43 -05:00
|
|
|
name = name.replace(/[[\]]/g, '\\$&');
|
|
|
|
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
|
|
|
const results = regex.exec(url);
|
|
|
|
if (!results) return null;
|
|
|
|
if (!results[2]) return '';
|
|
|
|
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
|
|
|
};
|
|
|
|
|
2017-01-18 17:19:51 -05:00
|
|
|
w.gl.utils.getSelectedFragment = () => {
|
2017-01-18 17:38:30 -05:00
|
|
|
const selection = window.getSelection();
|
2017-01-30 23:12:31 -05:00
|
|
|
if (selection.rangeCount === 0) return null;
|
2017-04-20 17:40:48 -04:00
|
|
|
const documentFragment = document.createDocumentFragment();
|
|
|
|
for (let i = 0; i < selection.rangeCount; i += 1) {
|
|
|
|
documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
|
|
|
|
}
|
2017-01-18 17:19:51 -05:00
|
|
|
if (documentFragment.textContent.length === 0) return null;
|
|
|
|
|
|
|
|
return documentFragment;
|
2017-01-18 17:38:30 -05:00
|
|
|
};
|
2017-01-18 17:19:51 -05:00
|
|
|
|
|
|
|
w.gl.utils.insertText = (target, text) => {
|
|
|
|
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
|
|
|
|
|
2017-01-18 17:38:30 -05:00
|
|
|
const selectionStart = target.selectionStart;
|
|
|
|
const selectionEnd = target.selectionEnd;
|
|
|
|
const value = target.value;
|
2017-01-18 17:19:51 -05:00
|
|
|
|
2017-01-18 17:38:30 -05:00
|
|
|
const textBefore = value.substring(0, selectionStart);
|
|
|
|
const textAfter = value.substring(selectionEnd, value.length);
|
2017-05-18 23:17:59 -04:00
|
|
|
|
|
|
|
const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
|
|
|
|
const newText = textBefore + insertedText + textAfter;
|
2017-01-18 17:19:51 -05:00
|
|
|
|
|
|
|
target.value = newText;
|
2017-05-18 23:17:59 -04:00
|
|
|
target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
|
2017-01-19 11:02:39 -05:00
|
|
|
|
|
|
|
// Trigger autosave
|
|
|
|
$(target).trigger('input');
|
|
|
|
|
|
|
|
// Trigger autosize
|
|
|
|
var event = document.createEvent('Event');
|
|
|
|
event.initEvent('autosize:update', true, false);
|
|
|
|
target.dispatchEvent(event);
|
2017-01-18 17:38:30 -05:00
|
|
|
};
|
2017-01-18 17:19:51 -05:00
|
|
|
|
|
|
|
w.gl.utils.nodeMatchesSelector = (node, selector) => {
|
2017-01-18 17:38:30 -05:00
|
|
|
const matches = Element.prototype.matches ||
|
2017-01-18 17:19:51 -05:00
|
|
|
Element.prototype.matchesSelector ||
|
|
|
|
Element.prototype.mozMatchesSelector ||
|
|
|
|
Element.prototype.msMatchesSelector ||
|
|
|
|
Element.prototype.oMatchesSelector ||
|
|
|
|
Element.prototype.webkitMatchesSelector;
|
|
|
|
|
|
|
|
if (matches) {
|
|
|
|
return matches.call(node, selector);
|
|
|
|
}
|
|
|
|
|
|
|
|
// IE11 doesn't support `node.matches(selector)`
|
|
|
|
|
|
|
|
let parentNode = node.parentNode;
|
|
|
|
if (!parentNode) {
|
|
|
|
parentNode = document.createElement('div');
|
|
|
|
node = node.cloneNode(true);
|
|
|
|
parentNode.appendChild(node);
|
|
|
|
}
|
|
|
|
|
2017-01-18 17:38:30 -05:00
|
|
|
const matchingNodes = parentNode.querySelectorAll(selector);
|
2017-01-18 17:19:51 -05:00
|
|
|
return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
|
2017-01-18 17:38:30 -05:00
|
|
|
};
|
2017-01-25 12:23:25 -05:00
|
|
|
|
2017-01-24 11:56:26 -05:00
|
|
|
/**
|
|
|
|
this will take in the headers from an API response and normalize them
|
|
|
|
this way we don't run into production issues when nginx gives us lowercased header keys
|
|
|
|
*/
|
|
|
|
w.gl.utils.normalizeHeaders = (headers) => {
|
|
|
|
const upperCaseHeaders = {};
|
|
|
|
|
|
|
|
Object.keys(headers).forEach((e) => {
|
|
|
|
upperCaseHeaders[e.toUpperCase()] = headers[e];
|
|
|
|
});
|
|
|
|
|
|
|
|
return upperCaseHeaders;
|
|
|
|
};
|
2017-02-05 12:36:00 -05:00
|
|
|
|
2017-03-30 08:14:32 -04:00
|
|
|
/**
|
|
|
|
this will take in the getAllResponseHeaders result and normalize them
|
|
|
|
this way we don't run into production issues when nginx gives us lowercased header keys
|
|
|
|
*/
|
|
|
|
w.gl.utils.normalizeCRLFHeaders = (headers) => {
|
|
|
|
const headersObject = {};
|
|
|
|
const headersArray = headers.split('\n');
|
|
|
|
|
|
|
|
headersArray.forEach((header) => {
|
|
|
|
const keyValue = header.split(': ');
|
|
|
|
headersObject[keyValue[0]] = keyValue[1];
|
|
|
|
});
|
|
|
|
|
|
|
|
return w.gl.utils.normalizeHeaders(headersObject);
|
|
|
|
};
|
|
|
|
|
2017-02-16 07:02:55 -05:00
|
|
|
/**
|
|
|
|
* Parses pagination object string values into numbers.
|
|
|
|
*
|
|
|
|
* @param {Object} paginationInformation
|
|
|
|
* @returns {Object}
|
|
|
|
*/
|
|
|
|
w.gl.utils.parseIntPagination = paginationInformation => ({
|
|
|
|
perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
|
|
|
|
page: parseInt(paginationInformation['X-PAGE'], 10),
|
|
|
|
total: parseInt(paginationInformation['X-TOTAL'], 10),
|
|
|
|
totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
|
|
|
|
nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
|
|
|
|
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
|
|
|
|
});
|
|
|
|
|
2017-02-13 11:11:11 -05:00
|
|
|
/**
|
2017-03-30 16:21:18 -04:00
|
|
|
* Updates the search parameter of a URL given the parameter and value provided.
|
2017-02-13 11:11:11 -05:00
|
|
|
*
|
|
|
|
* If no search params are present we'll add it.
|
|
|
|
* If param for page is already present, we'll update it
|
|
|
|
* If there are params but not for the given one, we'll add it at the end.
|
|
|
|
* Returns the new search parameters.
|
|
|
|
*
|
|
|
|
* @param {String} param
|
|
|
|
* @param {Number|String|Undefined|Null} value
|
|
|
|
* @return {String}
|
|
|
|
*/
|
|
|
|
w.gl.utils.setParamInURL = (param, value) => {
|
|
|
|
let search;
|
2017-02-16 06:29:45 -05:00
|
|
|
const locationSearch = window.location.search;
|
2017-02-13 11:11:11 -05:00
|
|
|
|
2017-03-30 16:21:18 -04:00
|
|
|
if (locationSearch.length) {
|
|
|
|
const parameters = locationSearch.substring(1, locationSearch.length)
|
|
|
|
.split('&')
|
|
|
|
.reduce((acc, element) => {
|
|
|
|
const val = element.split('=');
|
|
|
|
acc[val[0]] = decodeURIComponent(val[1]);
|
|
|
|
return acc;
|
|
|
|
}, {});
|
2017-02-13 11:11:11 -05:00
|
|
|
|
2017-03-30 16:21:18 -04:00
|
|
|
parameters[param] = value;
|
2017-02-13 11:11:11 -05:00
|
|
|
|
2017-03-30 16:21:18 -04:00
|
|
|
const toString = Object.keys(parameters)
|
|
|
|
.map(val => `${val}=${encodeURIComponent(parameters[val])}`)
|
|
|
|
.join('&');
|
|
|
|
|
|
|
|
search = `?${toString}`;
|
|
|
|
} else {
|
|
|
|
search = `?${param}=${value}`;
|
2017-02-13 11:11:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return search;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts permission provided as strings to booleans.
|
|
|
|
*
|
|
|
|
* @param {String} string
|
|
|
|
* @returns {Boolean}
|
|
|
|
*/
|
|
|
|
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
|
2017-02-24 11:28:30 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Back Off exponential algorithm
|
|
|
|
* backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
|
|
|
|
*
|
|
|
|
* @param {Function<next, stop>} fn function to be called
|
|
|
|
* @param {Number} timeout
|
|
|
|
* @return {Promise<Any, Error>}
|
|
|
|
* @example
|
|
|
|
* ```
|
|
|
|
* backOff(function (next, stop) {
|
|
|
|
* // Let's perform this function repeatedly for 60s or for the timeout provided.
|
|
|
|
*
|
|
|
|
* ourFunction()
|
|
|
|
* .then(function (result) {
|
|
|
|
* // continue if result is not what we need
|
|
|
|
* next();
|
|
|
|
*
|
|
|
|
* // when result is what we need let's stop with the repetions and jump out of the cycle
|
|
|
|
* stop(result);
|
|
|
|
* })
|
|
|
|
* .catch(function (error) {
|
|
|
|
* // if there is an error, we need to stop this with an error.
|
|
|
|
* stop(error);
|
|
|
|
* })
|
|
|
|
* }, 60000)
|
|
|
|
* .then(function (result) {})
|
|
|
|
* .catch(function (error) {
|
|
|
|
* // deal with errors passed to stop()
|
|
|
|
* })
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
w.gl.utils.backOff = (fn, timeout = 60000) => {
|
2017-02-27 06:26:30 -05:00
|
|
|
const maxInterval = 32000;
|
2017-02-24 11:28:30 -05:00
|
|
|
let nextInterval = 2000;
|
|
|
|
|
2017-02-27 06:26:30 -05:00
|
|
|
const startTime = Date.now();
|
2017-02-24 11:28:30 -05:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
|
|
|
|
|
|
|
|
const next = () => {
|
2017-02-27 06:26:30 -05:00
|
|
|
if (Date.now() - startTime < timeout) {
|
2017-02-24 11:28:30 -05:00
|
|
|
setTimeout(fn.bind(null, next, stop), nextInterval);
|
2017-02-27 06:26:30 -05:00
|
|
|
nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
|
2017-02-24 11:28:30 -05:00
|
|
|
} else {
|
|
|
|
reject(new Error('BACKOFF_TIMEOUT'));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
fn(next, stop);
|
|
|
|
});
|
|
|
|
};
|
2017-04-06 06:40:39 -04:00
|
|
|
|
2017-04-13 06:09:20 -04:00
|
|
|
w.gl.utils.setFavicon = (faviconPath) => {
|
|
|
|
if (faviconEl && faviconPath) {
|
|
|
|
faviconEl.setAttribute('href', faviconPath);
|
2017-04-06 06:40:39 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
w.gl.utils.resetFavicon = () => {
|
|
|
|
if (faviconEl) {
|
|
|
|
faviconEl.setAttribute('href', originalFavicon);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
w.gl.utils.setCiStatusFavicon = (pageUrl) => {
|
|
|
|
$.ajax({
|
|
|
|
url: pageUrl,
|
|
|
|
dataType: 'json',
|
|
|
|
success: function(data) {
|
2017-04-13 07:50:03 -04:00
|
|
|
if (data && data.favicon) {
|
2017-04-13 06:09:20 -04:00
|
|
|
gl.utils.setFavicon(data.favicon);
|
2017-04-06 06:40:39 -04:00
|
|
|
} else {
|
|
|
|
gl.utils.resetFavicon();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
error: function() {
|
|
|
|
gl.utils.resetFavicon();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2016-12-29 11:19:11 -05:00
|
|
|
})(window);
|
2017-02-10 01:50:50 -05:00
|
|
|
}).call(window);
|