common_utils.js 11.9 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 */
F
Fatih Acet 已提交
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
(function() {
  (function(w) {
    var base;
    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;
      }
    };
J
José Iván 已提交
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65

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

    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;
      }
    };

    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();
        }
      });
    };

66 67 68
    // 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() {
69 70 71
      var hash = w.gl.utils.getLocationHash();
      if (!hash) return;

72 73 74
      // This is required to handle non-unicode characters in hash
      hash = decodeURIComponent(hash);

75 76
      // scroll to user-generated markdown anchor if we cannot find a match
      if (document.getElementById(hash) === null) {
77
        var target = document.getElementById('user-content-' + hash);
78 79 80 81 82
        if (target && target.scrollIntoView) {
          target.scrollIntoView(true);
        }
      } else {
        // only adjust for fixedTabs when not targeting user-generated content
83
        var fixedTabs = document.querySelector('.js-tabs-affix');
84
        if (fixedTabs) {
85
          window.scrollBy(0, -fixedTabs.offsetHeight);
86 87
        }
      }
88
    };
J
José Iván 已提交
89

K
Kushal Pandya 已提交
90 91 92 93 94 95 96 97 98 99 100 101 102
    // 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
      );
    };

103 104 105
    gl.utils.getPagePath = function(index) {
      index = index || 0;
      return $('body').data('page').split(':')[index];
F
Fatih Acet 已提交
106
    };
107

108 109 110 111 112 113 114 115 116 117 118 119 120
    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;
    };

C
Clement Ho 已提交
121 122 123 124
    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 ?
      return window.location.search.slice(1).split('&');
C
Clement Ho 已提交
125
    };
C
Clement Ho 已提交
126

127 128 129 130
    gl.utils.isMetaKey = function(e) {
      return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
    };

131 132 133 134 135 136 137 138
    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;
    };

F
Fatih Acet 已提交
139
    gl.utils.scrollToElement = function($el) {
140 141 142 143
      var top = $el.offset().top;
      gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();

      return $('body, html').animate({
144
        scrollTop: top - (gl.mrTabsHeight)
145 146
      }, 200);
    };
F
Fatih Acet 已提交
147

R
Regis 已提交
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
    /**
      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
    */
    w.gl.utils.getParameterByName = (name) => {
      const url = window.location.href;
      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, ' '));
    };

D
Douwe Maan 已提交
163
    w.gl.utils.getSelectedFragment = () => {
D
Douwe Maan 已提交
164
      const selection = window.getSelection();
165
      if (selection.rangeCount === 0) return null;
D
Douwe Maan 已提交
166
      const documentFragment = selection.getRangeAt(0).cloneContents();
D
Douwe Maan 已提交
167 168 169
      if (documentFragment.textContent.length === 0) return null;

      return documentFragment;
D
Douwe Maan 已提交
170
    };
D
Douwe Maan 已提交
171 172 173 174

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

D
Douwe Maan 已提交
175 176 177
      const selectionStart = target.selectionStart;
      const selectionEnd = target.selectionEnd;
      const value = target.value;
D
Douwe Maan 已提交
178

D
Douwe Maan 已提交
179 180 181
      const textBefore = value.substring(0, selectionStart);
      const textAfter = value.substring(selectionEnd, value.length);
      const newText = textBefore + text + textAfter;
D
Douwe Maan 已提交
182 183 184

      target.value = newText;
      target.selectionStart = target.selectionEnd = selectionStart + text.length;
185 186 187 188 189 190 191 192

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

      // Trigger autosize
      var event = document.createEvent('Event');
      event.initEvent('autosize:update', true, false);
      target.dispatchEvent(event);
D
Douwe Maan 已提交
193
    };
D
Douwe Maan 已提交
194 195

    w.gl.utils.nodeMatchesSelector = (node, selector) => {
D
Douwe Maan 已提交
196
      const matches = Element.prototype.matches ||
D
Douwe Maan 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
        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);
      }

D
Douwe Maan 已提交
216
      const matchingNodes = parentNode.querySelectorAll(selector);
D
Douwe Maan 已提交
217
      return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
D
Douwe Maan 已提交
218
    };
219

220 221 222 223 224 225 226 227 228 229 230 231 232
    /**
      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;
    };
233

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
    /**
      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);
    };

250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
    /**
     * 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),
    });

265
    /**
266
     * Updates the search parameter of a URL given the parameter and value provided.
267 268 269 270 271 272 273 274 275 276 277 278
     *
     * 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;
F
Filipa Lacerda 已提交
279
      const locationSearch = window.location.search;
280

281 282 283 284 285 286 287 288
      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;
          }, {});
289

290
        parameters[param] = value;
291

292 293 294 295 296 297 298
        const toString = Object.keys(parameters)
          .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
          .join('&');

        search = `?${toString}`;
      } else {
        search = `?${param}=${value}`;
299 300 301 302 303 304 305 306 307 308 309 310
      }

      return search;
    };

    /**
     * Converts permission provided as strings to booleans.
     *
     * @param  {String} string
     * @returns {Boolean}
     */
    w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
F
Filipa Lacerda 已提交
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343

    /**
     * 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) => {
344
      const maxInterval = 32000;
F
Filipa Lacerda 已提交
345 346
      let nextInterval = 2000;

347
      const startTime = Date.now();
F
Filipa Lacerda 已提交
348 349 350 351 352

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

        const next = () => {
353
          if (Date.now() - startTime < timeout) {
F
Filipa Lacerda 已提交
354
            setTimeout(fn.bind(null, next, stop), nextInterval);
355
            nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
F
Filipa Lacerda 已提交
356 357 358 359 360 361 362 363
          } else {
            reject(new Error('BACKOFF_TIMEOUT'));
          }
        };

        fn(next, stop);
      });
    };
364
  })(window);
365
}).call(window);