756 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			756 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
/**
 | 
						|
 * @module common-utils
 | 
						|
 */
 | 
						|
 | 
						|
import $ from 'jquery';
 | 
						|
import axios from './axios_utils';
 | 
						|
import { getLocationHash } from './url_utility';
 | 
						|
import { convertToCamelCase } from './text_utility';
 | 
						|
import { isObject } from './type_utility';
 | 
						|
import BreakpointInstance from '../../breakpoints';
 | 
						|
 | 
						|
export const getPagePath = (index = 0) => {
 | 
						|
  const page = $('body').attr('data-page') || '';
 | 
						|
 | 
						|
  return page.split(':')[index];
 | 
						|
};
 | 
						|
 | 
						|
export const isInGroupsPage = () => getPagePath() === 'groups';
 | 
						|
 | 
						|
export const isInProjectPage = () => getPagePath() === 'projects';
 | 
						|
 | 
						|
export const getProjectSlug = () => {
 | 
						|
  if (isInProjectPage()) {
 | 
						|
    return $('body').data('project');
 | 
						|
  }
 | 
						|
  return null;
 | 
						|
};
 | 
						|
 | 
						|
export const getGroupSlug = () => {
 | 
						|
  if (isInGroupsPage()) {
 | 
						|
    return $('body').data('group');
 | 
						|
  }
 | 
						|
  return null;
 | 
						|
};
 | 
						|
 | 
						|
export const checkPageAndAction = (page, action) => {
 | 
						|
  const pagePath = getPagePath(1);
 | 
						|
  const actionPath = getPagePath(2);
 | 
						|
 | 
						|
  return pagePath === page && actionPath === action;
 | 
						|
};
 | 
						|
 | 
						|
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
 | 
						|
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
 | 
						|
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
 | 
						|
 | 
						|
export const ajaxGet = url =>
 | 
						|
  axios
 | 
						|
    .get(url, {
 | 
						|
      params: { format: 'js' },
 | 
						|
      responseType: 'text',
 | 
						|
    })
 | 
						|
    .then(({ data }) => {
 | 
						|
      $.globalEval(data);
 | 
						|
    });
 | 
						|
 | 
						|
export const rstrip = val => {
 | 
						|
  if (val) {
 | 
						|
    return val.replace(/\s+$/, '');
 | 
						|
  }
 | 
						|
  return val;
 | 
						|
};
 | 
						|
 | 
						|
export const updateTooltipTitle = ($tooltipEl, newTitle) =>
 | 
						|
  $tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
 | 
						|
 | 
						|
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
 | 
						|
  const field = $(fieldSelector);
 | 
						|
  const closestSubmit = field.closest('form').find(buttonSelector);
 | 
						|
  if (rstrip(field.val()) === '') {
 | 
						|
    closestSubmit.disable();
 | 
						|
  }
 | 
						|
  // eslint-disable-next-line func-names
 | 
						|
  return field.on(eventName, function() {
 | 
						|
    if (rstrip($(this).val()) === '') {
 | 
						|
      return closestSubmit.disable();
 | 
						|
    }
 | 
						|
    return closestSubmit.enable();
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
// automatically adjust scroll position for hash urls taking the height of the navbar into account
 | 
						|
// https://github.com/twitter/bootstrap/issues/1768
 | 
						|
export const handleLocationHash = () => {
 | 
						|
  let hash = getLocationHash();
 | 
						|
  if (!hash) return;
 | 
						|
 | 
						|
  // This is required to handle non-unicode characters in hash
 | 
						|
  hash = decodeURIComponent(hash);
 | 
						|
 | 
						|
  const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
 | 
						|
  const fixedTabs = document.querySelector('.js-tabs-affix');
 | 
						|
  const fixedDiffStats = document.querySelector('.js-diff-files-changed');
 | 
						|
  const fixedNav = document.querySelector('.navbar-gitlab');
 | 
						|
  const performanceBar = document.querySelector('#js-peek');
 | 
						|
  const topPadding = 8;
 | 
						|
 | 
						|
  let adjustment = 0;
 | 
						|
  if (fixedNav) adjustment -= fixedNav.offsetHeight;
 | 
						|
 | 
						|
  if (target && target.scrollIntoView) {
 | 
						|
    target.scrollIntoView(true);
 | 
						|
  }
 | 
						|
 | 
						|
  if (fixedTabs) {
 | 
						|
    adjustment -= fixedTabs.offsetHeight;
 | 
						|
  }
 | 
						|
 | 
						|
  if (fixedDiffStats) {
 | 
						|
    adjustment -= fixedDiffStats.offsetHeight;
 | 
						|
  }
 | 
						|
 | 
						|
  if (performanceBar) {
 | 
						|
    adjustment -= performanceBar.offsetHeight;
 | 
						|
  }
 | 
						|
 | 
						|
  if (isInMRPage()) {
 | 
						|
    adjustment -= topPadding;
 | 
						|
  }
 | 
						|
 | 
						|
  window.scrollBy(0, adjustment);
 | 
						|
};
 | 
						|
 | 
						|
// Check if element scrolled into viewport from above or below
 | 
						|
// Courtesy http://stackoverflow.com/a/7557433/414749
 | 
						|
export const isInViewport = (el, offset = {}) => {
 | 
						|
  const rect = el.getBoundingClientRect();
 | 
						|
  const { top, left } = offset;
 | 
						|
 | 
						|
  return (
 | 
						|
    rect.top >= (top || 0) &&
 | 
						|
    rect.left >= (left || 0) &&
 | 
						|
    rect.bottom <= window.innerHeight &&
 | 
						|
    parseInt(rect.right, 10) <= window.innerWidth
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const parseUrl = url => {
 | 
						|
  const parser = document.createElement('a');
 | 
						|
  parser.href = url;
 | 
						|
  return parser;
 | 
						|
};
 | 
						|
 | 
						|
export const parseUrlPathname = url => {
 | 
						|
  const parsedUrl = 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}`;
 | 
						|
};
 | 
						|
 | 
						|
const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
 | 
						|
 | 
						|
export const urlParamsToArray = (path = '') =>
 | 
						|
  splitPath(path)
 | 
						|
    .filter(param => param.length > 0)
 | 
						|
    .map(param => {
 | 
						|
      const split = param.split('=');
 | 
						|
      return [decodeURI(split[0]), split[1]].join('=');
 | 
						|
    });
 | 
						|
 | 
						|
export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
 | 
						|
 | 
						|
export const urlParamsToObject = (path = '') =>
 | 
						|
  splitPath(path).reduce((dataParam, filterParam) => {
 | 
						|
    if (filterParam === '') {
 | 
						|
      return dataParam;
 | 
						|
    }
 | 
						|
 | 
						|
    const data = dataParam;
 | 
						|
    let [key, value] = filterParam.split('=');
 | 
						|
    const isArray = key.includes('[]');
 | 
						|
    key = key.replace('[]', '');
 | 
						|
    value = decodeURIComponent(value.replace(/\+/g, ' '));
 | 
						|
 | 
						|
    if (isArray) {
 | 
						|
      if (!data[key]) {
 | 
						|
        data[key] = [];
 | 
						|
      }
 | 
						|
 | 
						|
      data[key].push(value);
 | 
						|
    } else {
 | 
						|
      data[key] = value;
 | 
						|
    }
 | 
						|
 | 
						|
    return data;
 | 
						|
  }, {});
 | 
						|
 | 
						|
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
 | 
						|
 | 
						|
// 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)
 | 
						|
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
 | 
						|
 | 
						|
export const contentTop = () => {
 | 
						|
  const perfBar = $('#js-peek').outerHeight() || 0;
 | 
						|
  const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0;
 | 
						|
  const headerHeight = $('.navbar-gitlab').outerHeight() || 0;
 | 
						|
  const diffFilesChanged = $('.js-diff-files-changed').outerHeight() || 0;
 | 
						|
  const mdScreenOrBigger = ['lg', 'md'].includes(BreakpointInstance.getBreakpointSize());
 | 
						|
  const diffFileTitleBar =
 | 
						|
    (mdScreenOrBigger && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0;
 | 
						|
  const compareVersionsHeaderHeight =
 | 
						|
    (mdScreenOrBigger && $('.mr-version-controls').outerHeight()) || 0;
 | 
						|
 | 
						|
  return (
 | 
						|
    perfBar +
 | 
						|
    mrTabsHeight +
 | 
						|
    headerHeight +
 | 
						|
    diffFilesChanged +
 | 
						|
    diffFileTitleBar +
 | 
						|
    compareVersionsHeaderHeight
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const scrollToElement = element => {
 | 
						|
  let $el = element;
 | 
						|
  if (!(element instanceof $)) {
 | 
						|
    $el = $(element);
 | 
						|
  }
 | 
						|
  const { top } = $el.offset();
 | 
						|
 | 
						|
  return $('body, html').animate(
 | 
						|
    {
 | 
						|
      scrollTop: top - contentTop(),
 | 
						|
    },
 | 
						|
    200,
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns a function that can only be invoked once between
 | 
						|
 * each browser screen repaint.
 | 
						|
 * @param {Function} fn
 | 
						|
 */
 | 
						|
export const debounceByAnimationFrame = fn => {
 | 
						|
  let requestId;
 | 
						|
 | 
						|
  return function debounced(...args) {
 | 
						|
    if (requestId) {
 | 
						|
      window.cancelAnimationFrame(requestId);
 | 
						|
    }
 | 
						|
    requestId = window.requestAnimationFrame(() => fn.apply(this, args));
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
  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
 | 
						|
*/
 | 
						|
export const getParameterByName = (name, urlToParse) => {
 | 
						|
  const url = urlToParse || window.location.href;
 | 
						|
  const parsedName = name.replace(/[[\]]/g, '\\$&');
 | 
						|
  const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`);
 | 
						|
  const results = regex.exec(url);
 | 
						|
  if (!results) return null;
 | 
						|
  if (!results[2]) return '';
 | 
						|
  return decodeURIComponent(results[2].replace(/\+/g, ' '));
 | 
						|
};
 | 
						|
 | 
						|
const handleSelectedRange = (range, restrictToNode) => {
 | 
						|
  // Make sure this range is within the restricting container
 | 
						|
  if (restrictToNode && !range.intersectsNode(restrictToNode)) return null;
 | 
						|
 | 
						|
  // If only a part of the range is within the wanted container, we need to restrict the range to it
 | 
						|
  if (restrictToNode && !restrictToNode.contains(range.commonAncestorContainer)) {
 | 
						|
    if (!restrictToNode.contains(range.startContainer)) range.setStart(restrictToNode, 0);
 | 
						|
    if (!restrictToNode.contains(range.endContainer))
 | 
						|
      range.setEnd(restrictToNode, restrictToNode.childNodes.length);
 | 
						|
  }
 | 
						|
 | 
						|
  const container = range.commonAncestorContainer;
 | 
						|
  // add context to fragment if needed
 | 
						|
  if (container.tagName === 'OL') {
 | 
						|
    const parentContainer = document.createElement(container.tagName);
 | 
						|
    parentContainer.appendChild(range.cloneContents());
 | 
						|
    return parentContainer;
 | 
						|
  }
 | 
						|
  return range.cloneContents();
 | 
						|
};
 | 
						|
 | 
						|
export const getSelectedFragment = restrictToNode => {
 | 
						|
  const selection = window.getSelection();
 | 
						|
  if (selection.rangeCount === 0) return null;
 | 
						|
  // Most usages of the selection only want text from a part of the page (e.g. discussion)
 | 
						|
  if (restrictToNode && !selection.containsNode(restrictToNode, true)) return null;
 | 
						|
 | 
						|
  const documentFragment = document.createDocumentFragment();
 | 
						|
  documentFragment.originalNodes = [];
 | 
						|
 | 
						|
  for (let i = 0; i < selection.rangeCount; i += 1) {
 | 
						|
    const range = selection.getRangeAt(i);
 | 
						|
    const handledRange = handleSelectedRange(range, restrictToNode);
 | 
						|
    if (handledRange) {
 | 
						|
      documentFragment.appendChild(handledRange);
 | 
						|
      documentFragment.originalNodes.push(range.commonAncestorContainer);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  if (documentFragment.textContent.length === 0) return null;
 | 
						|
 | 
						|
  return documentFragment;
 | 
						|
};
 | 
						|
 | 
						|
export const insertText = (target, text) => {
 | 
						|
  // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
 | 
						|
  const { selectionStart, selectionEnd, value } = target;
 | 
						|
 | 
						|
  const textBefore = value.substring(0, selectionStart);
 | 
						|
  const textAfter = value.substring(selectionEnd, value.length);
 | 
						|
 | 
						|
  const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
 | 
						|
  const newText = textBefore + insertedText + textAfter;
 | 
						|
 | 
						|
  // eslint-disable-next-line no-param-reassign
 | 
						|
  target.value = newText;
 | 
						|
  // eslint-disable-next-line no-param-reassign
 | 
						|
  target.selectionStart = selectionStart + insertedText.length;
 | 
						|
 | 
						|
  // eslint-disable-next-line no-param-reassign
 | 
						|
  target.selectionEnd = selectionStart + insertedText.length;
 | 
						|
 | 
						|
  // Trigger autosave
 | 
						|
  target.dispatchEvent(new Event('input'));
 | 
						|
 | 
						|
  // Trigger autosize
 | 
						|
  const event = document.createEvent('Event');
 | 
						|
  event.initEvent('autosize:update', true, false);
 | 
						|
  target.dispatchEvent(event);
 | 
						|
};
 | 
						|
 | 
						|
export const nodeMatchesSelector = (node, selector) => {
 | 
						|
  const matches =
 | 
						|
    Element.prototype.matches ||
 | 
						|
    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;
 | 
						|
 | 
						|
  if (!parentNode) {
 | 
						|
    parentNode = document.createElement('div');
 | 
						|
    // eslint-disable-next-line no-param-reassign
 | 
						|
    node = node.cloneNode(true);
 | 
						|
    parentNode.appendChild(node);
 | 
						|
  }
 | 
						|
 | 
						|
  const matchingNodes = parentNode.querySelectorAll(selector);
 | 
						|
  return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
  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
 | 
						|
*/
 | 
						|
export const normalizeHeaders = headers => {
 | 
						|
  const upperCaseHeaders = {};
 | 
						|
 | 
						|
  Object.keys(headers || {}).forEach(e => {
 | 
						|
    upperCaseHeaders[e.toUpperCase()] = headers[e];
 | 
						|
  });
 | 
						|
 | 
						|
  return upperCaseHeaders;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
  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
 | 
						|
*/
 | 
						|
export const normalizeCRLFHeaders = headers => {
 | 
						|
  const headersObject = {};
 | 
						|
  const headersArray = headers.split('\n');
 | 
						|
 | 
						|
  headersArray.forEach(header => {
 | 
						|
    const keyValue = header.split(': ');
 | 
						|
 | 
						|
    // eslint-disable-next-line prefer-destructuring
 | 
						|
    headersObject[keyValue[0]] = keyValue[1];
 | 
						|
  });
 | 
						|
 | 
						|
  return normalizeHeaders(headersObject);
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Parses pagination object string values into numbers.
 | 
						|
 *
 | 
						|
 * @param {Object} paginationInformation
 | 
						|
 * @returns {Object}
 | 
						|
 */
 | 
						|
export const 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),
 | 
						|
});
 | 
						|
 | 
						|
/**
 | 
						|
 * Given a string of query parameters creates an object.
 | 
						|
 *
 | 
						|
 * @example
 | 
						|
 * `scope=all&page=2` -> { scope: 'all', page: '2'}
 | 
						|
 * `scope=all` -> { scope: 'all' }
 | 
						|
 * ``-> {}
 | 
						|
 * @param {String} query
 | 
						|
 * @returns {Object}
 | 
						|
 */
 | 
						|
export const parseQueryStringIntoObject = (query = '') => {
 | 
						|
  if (query === '') return {};
 | 
						|
 | 
						|
  return query.split('&').reduce((acc, element) => {
 | 
						|
    const val = element.split('=');
 | 
						|
    Object.assign(acc, {
 | 
						|
      [val[0]]: decodeURIComponent(val[1]),
 | 
						|
    });
 | 
						|
    return acc;
 | 
						|
  }, {});
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Converts object with key-value pairs
 | 
						|
 * into query-param string
 | 
						|
 *
 | 
						|
 * @param {Object} params
 | 
						|
 */
 | 
						|
export const objectToQueryString = (params = {}) =>
 | 
						|
  Object.keys(params)
 | 
						|
    .map(param => `${param}=${params[param]}`)
 | 
						|
    .join('&');
 | 
						|
 | 
						|
export const buildUrlWithCurrentLocation = param => {
 | 
						|
  if (param) return `${window.location.pathname}${param}`;
 | 
						|
 | 
						|
  return window.location.pathname;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Based on the current location and the string parameters provided
 | 
						|
 * creates a new entry in the history without reloading the page.
 | 
						|
 *
 | 
						|
 * @param {String} param
 | 
						|
 */
 | 
						|
export const historyPushState = newUrl => {
 | 
						|
  window.history.pushState({}, document.title, newUrl);
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns true for a String value of "true" and false otherwise.
 | 
						|
 * This is the opposite of Boolean(...).toString().
 | 
						|
 * `parseBoolean` is idempotent.
 | 
						|
 *
 | 
						|
 * @param  {String} value
 | 
						|
 * @returns {Boolean}
 | 
						|
 */
 | 
						|
export const parseBoolean = value => (value && value.toString()) === 'true';
 | 
						|
 | 
						|
/**
 | 
						|
 * @callback backOffCallback
 | 
						|
 * @param {Function} next
 | 
						|
 * @param {Function} stop
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Back Off exponential algorithm
 | 
						|
 * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
 | 
						|
 *
 | 
						|
 * @param {backOffCallback} 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()
 | 
						|
 *  })
 | 
						|
 * ```
 | 
						|
 */
 | 
						|
export const backOff = (fn, timeout = 60000) => {
 | 
						|
  const maxInterval = 32000;
 | 
						|
  let nextInterval = 2000;
 | 
						|
  let timeElapsed = 0;
 | 
						|
 | 
						|
  return new Promise((resolve, reject) => {
 | 
						|
    const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
 | 
						|
 | 
						|
    const next = () => {
 | 
						|
      if (timeElapsed < timeout) {
 | 
						|
        setTimeout(() => fn(next, stop), nextInterval);
 | 
						|
        timeElapsed += nextInterval;
 | 
						|
        nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
 | 
						|
      } else {
 | 
						|
        reject(new Error('BACKOFF_TIMEOUT'));
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    fn(next, stop);
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
export const createOverlayIcon = (iconPath, overlayPath) => {
 | 
						|
  const faviconImage = document.createElement('img');
 | 
						|
 | 
						|
  return new Promise(resolve => {
 | 
						|
    faviconImage.onload = () => {
 | 
						|
      const size = 32;
 | 
						|
 | 
						|
      const canvas = document.createElement('canvas');
 | 
						|
      canvas.width = size;
 | 
						|
      canvas.height = size;
 | 
						|
 | 
						|
      const context = canvas.getContext('2d');
 | 
						|
      context.clearRect(0, 0, size, size);
 | 
						|
      context.drawImage(
 | 
						|
        faviconImage,
 | 
						|
        0,
 | 
						|
        0,
 | 
						|
        faviconImage.width,
 | 
						|
        faviconImage.height,
 | 
						|
        0,
 | 
						|
        0,
 | 
						|
        size,
 | 
						|
        size,
 | 
						|
      );
 | 
						|
 | 
						|
      const overlayImage = document.createElement('img');
 | 
						|
      overlayImage.onload = () => {
 | 
						|
        context.drawImage(
 | 
						|
          overlayImage,
 | 
						|
          0,
 | 
						|
          0,
 | 
						|
          overlayImage.width,
 | 
						|
          overlayImage.height,
 | 
						|
          0,
 | 
						|
          0,
 | 
						|
          size,
 | 
						|
          size,
 | 
						|
        );
 | 
						|
 | 
						|
        const faviconWithOverlayUrl = canvas.toDataURL();
 | 
						|
 | 
						|
        resolve(faviconWithOverlayUrl);
 | 
						|
      };
 | 
						|
      overlayImage.src = overlayPath;
 | 
						|
    };
 | 
						|
    faviconImage.src = iconPath;
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
export const setFaviconOverlay = overlayPath => {
 | 
						|
  const faviconEl = document.getElementById('favicon');
 | 
						|
 | 
						|
  if (!faviconEl) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  const iconPath = faviconEl.getAttribute('data-original-href');
 | 
						|
 | 
						|
  return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl =>
 | 
						|
    faviconEl.setAttribute('href', faviconWithOverlayUrl),
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const setFavicon = faviconPath => {
 | 
						|
  const faviconEl = document.getElementById('favicon');
 | 
						|
  if (faviconEl && faviconPath) {
 | 
						|
    faviconEl.setAttribute('href', faviconPath);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
export const resetFavicon = () => {
 | 
						|
  const faviconEl = document.getElementById('favicon');
 | 
						|
 | 
						|
  if (faviconEl) {
 | 
						|
    const originalFavicon = faviconEl.getAttribute('data-original-href');
 | 
						|
    faviconEl.setAttribute('href', originalFavicon);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
export const setCiStatusFavicon = pageUrl =>
 | 
						|
  axios
 | 
						|
    .get(pageUrl)
 | 
						|
    .then(({ data }) => {
 | 
						|
      if (data && data.favicon) {
 | 
						|
        return setFaviconOverlay(data.favicon);
 | 
						|
      }
 | 
						|
      return resetFavicon();
 | 
						|
    })
 | 
						|
    .catch(error => {
 | 
						|
      resetFavicon();
 | 
						|
      throw error;
 | 
						|
    });
 | 
						|
 | 
						|
export const spriteIcon = (icon, className = '') => {
 | 
						|
  const classAttribute = className.length > 0 ? `class="${className}"` : '';
 | 
						|
 | 
						|
  return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * This method takes in object with snake_case property names
 | 
						|
 * and returns a new object with camelCase property names
 | 
						|
 *
 | 
						|
 * Reasoning for this method is to ensure consistent property
 | 
						|
 * naming conventions across JS code.
 | 
						|
 *
 | 
						|
 * This method also supports additional params in `options` object
 | 
						|
 *
 | 
						|
 * @param {Object} obj - Object to be converted.
 | 
						|
 * @param {Object} options - Object containing additional options.
 | 
						|
 * @param {boolean} options.deep - FLag to allow deep object converting
 | 
						|
 * @param {Array[]} dropKeys - List of properties to discard while building new object
 | 
						|
 * @param {Array[]} ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
 | 
						|
 */
 | 
						|
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
 | 
						|
  if (obj === null) {
 | 
						|
    return {};
 | 
						|
  }
 | 
						|
 | 
						|
  const initial = Array.isArray(obj) ? [] : {};
 | 
						|
  const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options;
 | 
						|
 | 
						|
  return Object.keys(obj).reduce((acc, prop) => {
 | 
						|
    const result = acc;
 | 
						|
    const val = obj[prop];
 | 
						|
 | 
						|
    // Drop properties from new object if
 | 
						|
    // there are any mentioned in options
 | 
						|
    if (dropKeys.indexOf(prop) > -1) {
 | 
						|
      return acc;
 | 
						|
    }
 | 
						|
 | 
						|
    // Skip converting properties in new object
 | 
						|
    // if there are any mentioned in options
 | 
						|
    if (ignoreKeyNames.indexOf(prop) > -1) {
 | 
						|
      result[prop] = obj[prop];
 | 
						|
      return acc;
 | 
						|
    }
 | 
						|
 | 
						|
    if (deep && (isObject(val) || Array.isArray(val))) {
 | 
						|
      result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
 | 
						|
    } else {
 | 
						|
      result[convertToCamelCase(prop)] = obj[prop];
 | 
						|
    }
 | 
						|
    return acc;
 | 
						|
  }, initial);
 | 
						|
};
 | 
						|
 | 
						|
export const imagePath = imgUrl =>
 | 
						|
  `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
 | 
						|
 | 
						|
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
 | 
						|
  // Click a .js-select-on-focus field, select the contents
 | 
						|
  // Prevent a mouseup event from deselecting the input
 | 
						|
  $(selector).on('focusin', function selectOnFocusCallback() {
 | 
						|
    $(this)
 | 
						|
      .select()
 | 
						|
      .one('mouseup', e => {
 | 
						|
        e.preventDefault();
 | 
						|
      });
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Method to round of values with decimal places
 | 
						|
 * with provided precision.
 | 
						|
 *
 | 
						|
 * Taken from https://stackoverflow.com/a/7343013/414749
 | 
						|
 *
 | 
						|
 * Eg; roundOffFloat(3.141592, 3) = 3.142
 | 
						|
 *
 | 
						|
 * Refer to spec/javascripts/lib/utils/common_utils_spec.js for
 | 
						|
 * more supported examples.
 | 
						|
 *
 | 
						|
 * @param {Float} number
 | 
						|
 * @param {Number} precision
 | 
						|
 */
 | 
						|
export const roundOffFloat = (number, precision = 0) => {
 | 
						|
  // eslint-disable-next-line no-restricted-properties
 | 
						|
  const multiplier = Math.pow(10, precision);
 | 
						|
  return Math.round(number * multiplier) / multiplier;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Represents navigation type constants of the Performance Navigation API.
 | 
						|
 * Detailed explanation see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation.
 | 
						|
 */
 | 
						|
export const NavigationType = {
 | 
						|
  TYPE_NAVIGATE: 0,
 | 
						|
  TYPE_RELOAD: 1,
 | 
						|
  TYPE_BACK_FORWARD: 2,
 | 
						|
  TYPE_RESERVED: 255,
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns the value of `gon.ee`
 | 
						|
 * Used to check if it's the EE codebase or the CE one.
 | 
						|
 *
 | 
						|
 * @returns Boolean
 | 
						|
 */
 | 
						|
export const isEE = () => window.gon && window.gon.ee;
 | 
						|
 | 
						|
window.gl = window.gl || {};
 | 
						|
window.gl.utils = {
 | 
						|
  ...(window.gl.utils || {}),
 | 
						|
  getPagePath,
 | 
						|
  isInGroupsPage,
 | 
						|
  isInProjectPage,
 | 
						|
  getProjectSlug,
 | 
						|
  getGroupSlug,
 | 
						|
  isInIssuePage,
 | 
						|
  ajaxGet,
 | 
						|
  rstrip,
 | 
						|
  updateTooltipTitle,
 | 
						|
  disableButtonIfEmptyField,
 | 
						|
  handleLocationHash,
 | 
						|
  isInViewport,
 | 
						|
  parseUrl,
 | 
						|
  parseUrlPathname,
 | 
						|
  getUrlParamsArray,
 | 
						|
  isMetaKey,
 | 
						|
  isMetaClick,
 | 
						|
  scrollToElement,
 | 
						|
  getParameterByName,
 | 
						|
  getSelectedFragment,
 | 
						|
  insertText,
 | 
						|
  nodeMatchesSelector,
 | 
						|
  spriteIcon,
 | 
						|
  imagePath,
 | 
						|
};
 |