BigW Consortium Gitlab

common_utils.js 13.7 KB
Newer Older
1
/* 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 */
Fatih Acet committed
2 3 4
(function() {
  (function(w) {
    var base;
5 6
    const faviconEl = document.getElementById('favicon');
    const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
Fatih Acet committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
    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;
      }
    };
29 30 31 32 33 34 35 36 37

    w.gl.utils.ajaxGet = function(url) {
      return $.ajax({
        type: "GET",
        url: url,
        dataType: "script"
      });
    };

38 39 40 41 42 43 44 45
    w.gl.utils.ajaxPost = function(url, data) {
      return $.ajax({
        type: 'POST',
        url: url,
        data: data,
      });
    };

46 47 48 49 50 51 52 53 54 55 56 57
    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;
      }
    };

58 59 60 61
    gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
      return $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
    };

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
    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();
        }
      });
    };

80 81 82
    // 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() {
83 84 85
      var hash = w.gl.utils.getLocationHash();
      if (!hash) return;

86 87 88
      // This is required to handle non-unicode characters in hash
      hash = decodeURIComponent(hash);

89 90 91
      const fixedTabs = document.querySelector('.js-tabs-affix');
      const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
      const fixedNav = document.querySelector('.navbar-gitlab');
92 93 94 95

      var adjustment = 0;
      if (fixedNav) adjustment -= fixedNav.offsetHeight;

96 97
      // scroll to user-generated markdown anchor if we cannot find a match
      if (document.getElementById(hash) === null) {
98
        var target = document.getElementById('user-content-' + hash);
99 100
        if (target && target.scrollIntoView) {
          target.scrollIntoView(true);
101
          window.scrollBy(0, adjustment);
102 103 104 105
        }
      } else {
        // only adjust for fixedTabs when not targeting user-generated content
        if (fixedTabs) {
106
          adjustment -= fixedTabs.offsetHeight;
107
        }
108 109 110 111 112

        if (fixedDiffStats) {
          adjustment -= fixedDiffStats.offsetHeight;
        }

113
        window.scrollBy(0, adjustment);
114
      }
115
    };
116

Kushal Pandya committed
117 118 119 120 121 122 123 124 125 126 127 128 129
    // 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
      );
    };

130 131 132
    gl.utils.getPagePath = function(index) {
      index = index || 0;
      return $('body').data('page').split(':')[index];
Fatih Acet committed
133
    };
134

135 136 137 138 139 140 141 142 143 144 145 146 147
    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;
    };

148 149 150
    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 ?
151 152 153 154
      return window.location.search.slice(1).split('&').map((param) => {
        const split = param.split('=');
        return [decodeURI(split[0]), split[1]].join('=');
      });
Clement Ho committed
155
    };
156

157 158 159 160
    gl.utils.isMetaKey = function(e) {
      return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
    };

161 162 163 164 165 166 167 168
    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;
    };

Fatih Acet committed
169
    gl.utils.scrollToElement = function($el) {
170 171 172 173
      var top = $el.offset().top;
      gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();

      return $('body, html').animate({
174
        scrollTop: top - (gl.mrTabsHeight)
175 176
      }, 200);
    };
Fatih Acet committed
177

178 179 180 181 182
    /**
      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
    */
183 184
    w.gl.utils.getParameterByName = (name, parseUrl) => {
      const url = parseUrl || window.location.href;
185 186 187 188 189 190 191 192
      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, ' '));
    };

193
    w.gl.utils.getSelectedFragment = () => {
Douwe Maan committed
194
      const selection = window.getSelection();
195
      if (selection.rangeCount === 0) return null;
196 197 198 199
      const documentFragment = document.createDocumentFragment();
      for (let i = 0; i < selection.rangeCount; i += 1) {
        documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
      }
200 201 202
      if (documentFragment.textContent.length === 0) return null;

      return documentFragment;
Douwe Maan committed
203
    };
204 205 206 207

    w.gl.utils.insertText = (target, text) => {
      // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas

Douwe Maan committed
208 209 210
      const selectionStart = target.selectionStart;
      const selectionEnd = target.selectionEnd;
      const value = target.value;
211

Douwe Maan committed
212 213
      const textBefore = value.substring(0, selectionStart);
      const textAfter = value.substring(selectionEnd, value.length);
214 215 216

      const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
      const newText = textBefore + insertedText + textAfter;
217 218

      target.value = newText;
219
      target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
220 221 222 223 224 225 226 227

      // Trigger autosave
      $(target).trigger('input');

      // Trigger autosize
      var event = document.createEvent('Event');
      event.initEvent('autosize:update', true, false);
      target.dispatchEvent(event);
Douwe Maan committed
228
    };
229 230

    w.gl.utils.nodeMatchesSelector = (node, selector) => {
Douwe Maan committed
231
      const matches = Element.prototype.matches ||
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
        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);
      }

Douwe Maan committed
251
      const matchingNodes = parentNode.querySelectorAll(selector);
252
      return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
Douwe Maan committed
253
    };
254

255 256 257 258 259 260 261 262 263 264 265 266 267
    /**
      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;
    };
268

269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
    /**
      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);
    };

285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
    /**
     * 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),
    });

300
    /**
301
     * Updates the search parameter of a URL given the parameter and value provided.
302 303 304 305 306 307 308 309 310 311 312 313
     *
     * 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;
Filipa Lacerda committed
314
      const locationSearch = window.location.search;
315

316 317 318 319 320 321 322 323
      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;
          }, {});
324

325
        parameters[param] = value;
326

327 328 329 330 331 332 333
        const toString = Object.keys(parameters)
          .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
          .join('&');

        search = `?${toString}`;
      } else {
        search = `?${param}=${value}`;
334 335 336 337 338 339 340 341 342 343 344 345
      }

      return search;
    };

    /**
     * Converts permission provided as strings to booleans.
     *
     * @param  {String} string
     * @returns {Boolean}
     */
    w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378

    /**
     * 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) => {
379
      const maxInterval = 32000;
380
      let nextInterval = 2000;
381
      let timeElapsed = 0;
382 383 384 385 386

      return new Promise((resolve, reject) => {
        const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));

        const next = () => {
387 388 389
          if (timeElapsed < timeout) {
            setTimeout(() => fn(next, stop), nextInterval);
            timeElapsed += nextInterval;
390
            nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
391 392 393 394 395 396 397 398
          } else {
            reject(new Error('BACKOFF_TIMEOUT'));
          }
        };

        fn(next, stop);
      });
    };
399

400 401 402
    w.gl.utils.setFavicon = (faviconPath) => {
      if (faviconEl && faviconPath) {
        faviconEl.setAttribute('href', faviconPath);
403 404 405 406 407 408 409 410 411 412 413 414 415 416
      }
    };

    w.gl.utils.resetFavicon = () => {
      if (faviconEl) {
        faviconEl.setAttribute('href', originalFavicon);
      }
    };

    w.gl.utils.setCiStatusFavicon = (pageUrl) => {
      $.ajax({
        url: pageUrl,
        dataType: 'json',
        success: function(data) {
Luke "Jared" Bennett committed
417
          if (data && data.favicon) {
418
            gl.utils.setFavicon(data.favicon);
419 420 421 422 423 424 425 426 427
          } else {
            gl.utils.resetFavicon();
          }
        },
        error: function() {
          gl.utils.resetFavicon();
        }
      });
    };
428
  })(window);
429
}).call(window);