BigW Consortium Gitlab

search_autocomplete.js 14 KB
Newer Older
1
/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
2

3
((global) => {
4 5 6 7 8 9 10 11 12
  const KEYCODE = {
    ESCAPE: 27,
    BACKSPACE: 8,
    ENTER: 13,
    UP: 38,
    DOWN: 40
  };

  class SearchAutocomplete {
13
    constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
14
      this.bindEventContext();
15
      this.wrap = wrap || $('.search');
16 17 18 19 20
      this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
      this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
      this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
      this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
      this.dropdown = this.wrap.find('.dropdown');
Fatih Acet committed
21 22 23 24 25 26 27 28 29 30
      this.dropdownContent = this.dropdown.find('.dropdown-content');
      this.locationBadgeEl = this.getElement('.location-badge');
      this.scopeInputEl = this.getElement('#scope');
      this.searchInput = this.getElement('.search-input');
      this.projectInputEl = this.getElement('#search_project_id');
      this.groupInputEl = this.getElement('#group_id');
      this.searchCodeInputEl = this.getElement('#search_code');
      this.repositoryInputEl = this.getElement('#repository_ref');
      this.clearInput = this.getElement('.js-clear-input');
      this.saveOriginalState();
31
      // Only when user is logged in
Fatih Acet committed
32 33 34 35 36 37 38 39
      if (gon.current_user_id) {
        this.createAutocomplete();
      }
      this.searchInput.addClass('disabled');
      this.saveTextLength();
      this.bindEvents();
    }

40
    // Finds an element inside wrapper element
41 42 43 44 45 46 47 48
    bindEventContext() {
      this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
      this.onClearInputClick = this.onClearInputClick.bind(this);
      this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
      this.onSearchInputClick = this.onSearchInputClick.bind(this);
      this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
      this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
    }
49
    getElement(selector) {
Fatih Acet committed
50
      return this.wrap.find(selector);
51
    }
Fatih Acet committed
52

53
    saveOriginalState() {
Fatih Acet committed
54
      return this.originalState = this.serializeState();
55
    }
Fatih Acet committed
56

57
    saveTextLength() {
Fatih Acet committed
58
      return this.lastTextLength = this.searchInput.val().length;
59
    }
Fatih Acet committed
60

61
    createAutocomplete() {
Fatih Acet committed
62 63 64 65 66 67 68 69 70 71
      return this.searchInput.glDropdown({
        filterInputBlur: false,
        filterable: true,
        filterRemote: true,
        highlight: true,
        enterCallback: false,
        filterInput: 'input#search',
        search: {
          fields: ['text']
        },
72
        id: this.getSearchText,
Fatih Acet committed
73 74 75 76
        data: this.getData.bind(this),
        selectable: true,
        clicked: this.onClick.bind(this)
      });
77
    }
Fatih Acet committed
78

79 80 81 82
    getSearchText(selectedObject, el) {
      return selectedObject.id ? selectedObject.text : '';
    }

83
    getData(term, callback) {
Fatih Acet committed
84 85 86 87 88 89 90 91 92
      var _this, contents, jqXHR;
      _this = this;
      if (!term) {
        if (contents = this.getCategoryContents()) {
          this.searchInput.data('glDropdown').filter.options.callback(contents);
          this.enableAutocomplete();
        }
        return;
      }
93
      // Prevent multiple ajax calls
Fatih Acet committed
94 95 96 97 98 99 100 101 102 103
      if (this.loadingSuggestions) {
        return;
      }
      this.loadingSuggestions = true;
      return jqXHR = $.get(this.autocompletePath, {
        project_id: this.projectId,
        project_ref: this.projectRef,
        term: term
      }, function(response) {
        var data, firstCategory, i, lastCategory, len, suggestion;
104
        // Hide dropdown menu if no suggestions returns
Fatih Acet committed
105 106 107 108 109
        if (!response.length) {
          _this.disableAutocomplete();
          return;
        }
        data = [];
110
        // List results
Fatih Acet committed
111
        firstCategory = true;
112
        for (i = 0, len = response.length; i < len; i += 1) {
Fatih Acet committed
113
          suggestion = response[i];
114
          // Add group header before list each group
Fatih Acet committed
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
          if (lastCategory !== suggestion.category) {
            if (!firstCategory) {
              data.push('separator');
            }
            if (firstCategory) {
              firstCategory = false;
            }
            data.push({
              header: suggestion.category
            });
            lastCategory = suggestion.category;
          }
          data.push({
            id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
            category: suggestion.category,
            text: suggestion.label,
            url: suggestion.url
          });
        }
134
        // Add option to proceed with the search
Fatih Acet committed
135 136 137 138 139 140 141 142 143 144 145
        if (data.length) {
          data.push('separator');
          data.push({
            text: "Result name contains \"" + term + "\"",
            url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val())
          });
        }
        return callback(data);
      }).always(function() {
        return _this.loadingSuggestions = false;
      });
146
    }
Fatih Acet committed
147

148
    getCategoryContents() {
Clement Ho committed
149
      var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
Fatih Acet committed
150
      userId = gon.current_user_id;
Clement Ho committed
151
      userName = gon.current_username;
Fatih Acet committed
152 153 154 155 156 157 158 159 160 161 162 163 164 165
      utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
      if (utils.isInGroupsPage() && groupOptions) {
        options = groupOptions[utils.getGroupSlug()];
      } else if (utils.isInProjectPage() && projectOptions) {
        options = projectOptions[utils.getProjectSlug()];
      } else if (dashboardOptions) {
        options = dashboardOptions;
      }
      issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name;
      items = [
        {
          header: "" + name
        }, {
          text: 'Issues assigned to me',
Clement Ho committed
166
          url: issuesPath + "/?assignee_username=" + userName
Fatih Acet committed
167 168
        }, {
          text: "Issues I've created",
Clement Ho committed
169
          url: issuesPath + "/?author_username=" + userName
Fatih Acet committed
170 171
        }, 'separator', {
          text: 'Merge requests assigned to me',
172
          url: mrPath + "/?assignee_username=" + userName
Fatih Acet committed
173 174
        }, {
          text: "Merge requests I've created",
175
          url: mrPath + "/?author_username=" + userName
Fatih Acet committed
176 177 178 179 180 181
        }
      ];
      if (!name) {
        items.splice(0, 1);
      }
      return items;
182
    }
Fatih Acet committed
183

184
    serializeState() {
Fatih Acet committed
185
      return {
186
        // Search Criteria
Fatih Acet committed
187 188 189 190 191
        search_project_id: this.projectInputEl.val(),
        group_id: this.groupInputEl.val(),
        search_code: this.searchCodeInputEl.val(),
        repository_ref: this.repositoryInputEl.val(),
        scope: this.scopeInputEl.val(),
192
        // Location badge
Fatih Acet committed
193 194
        _location: this.locationBadgeEl.text()
      };
195
    }
Fatih Acet committed
196

197
    bindEvents() {
Fatih Acet committed
198 199 200 201 202 203 204 205 206 207 208
      this.searchInput.on('keydown', this.onSearchInputKeyDown);
      this.searchInput.on('keyup', this.onSearchInputKeyUp);
      this.searchInput.on('click', this.onSearchInputClick);
      this.searchInput.on('focus', this.onSearchInputFocus);
      this.searchInput.on('blur', this.onSearchInputBlur);
      this.clearInput.on('click', this.onClearInputClick);
      return this.locationBadgeEl.on('click', (function(_this) {
        return function() {
          return _this.searchInput.focus();
        };
      })(this));
209
    }
Fatih Acet committed
210

211
    enableAutocomplete() {
Fatih Acet committed
212
      var _this;
213
      // No need to enable anything if user is not logged in
Fatih Acet committed
214 215 216 217 218 219 220 221 222
      if (!gon.current_user_id) {
        return;
      }
      if (!this.dropdown.hasClass('open')) {
        _this = this;
        this.loadingSuggestions = false;
        this.dropdown.addClass('open').trigger('shown.bs.dropdown');
        return this.searchInput.removeClass('disabled');
      }
223
    }
Fatih Acet committed
224

225
      // Saves last length of the entered text
226
    onSearchInputKeyDown() {
Fatih Acet committed
227
      return this.saveTextLength();
228
    }
Fatih Acet committed
229

230
    onSearchInputKeyUp(e) {
Fatih Acet committed
231 232
      switch (e.keyCode) {
        case KEYCODE.BACKSPACE:
233
          // when trying to remove the location badge
Fatih Acet committed
234 235 236
          if (this.lastTextLength === 0 && this.badgePresent()) {
            this.removeLocationBadge();
          }
237
          // When removing the last character and no badge is present
Fatih Acet committed
238 239 240
          if (this.lastTextLength === 1) {
            this.disableAutocomplete();
          }
241
          // When removing any character from existin value
Fatih Acet committed
242 243 244 245 246 247 248
          if (this.lastTextLength > 1) {
            this.enableAutocomplete();
          }
          break;
        case KEYCODE.ESCAPE:
          this.restoreOriginalState();
          break;
249
        case KEYCODE.ENTER:
Luke Bennett committed
250
          this.disableAutocomplete();
251
          break;
Luke Bennett committed
252
        case KEYCODE.UP:
253
        case KEYCODE.DOWN:
Luke Bennett committed
254
          return;
Fatih Acet committed
255
        default:
256 257
          // Handle the case when deleting the input value other than backspace
          // e.g. Pressing ctrl + backspace or ctrl + x
Fatih Acet committed
258 259 260
          if (this.searchInput.val() === '') {
            this.disableAutocomplete();
          } else {
261
            // We should display the menu only when input is not empty
Fatih Acet committed
262 263 264 265 266 267
            if (e.keyCode !== KEYCODE.ENTER) {
              this.enableAutocomplete();
            }
          }
      }
      this.wrap.toggleClass('has-value', !!e.target.value);
268
    }
Fatih Acet committed
269

270
    // Avoid falsy value to be returned
271
    onSearchInputClick(e) {
Fatih Acet committed
272
      return e.stopImmediatePropagation();
273
    }
Fatih Acet committed
274

275
    onSearchInputFocus() {
Fatih Acet committed
276 277 278 279 280
      this.isFocused = true;
      this.wrap.addClass('search-active');
      if (this.getValue() === '') {
        return this.getData();
      }
281
    }
Fatih Acet committed
282

283
    getValue() {
Fatih Acet committed
284
      return this.searchInput.val();
285
    }
Fatih Acet committed
286

287
    onClearInputClick(e) {
Fatih Acet committed
288 289
      e.preventDefault();
      return this.searchInput.val('').focus();
290
    }
Fatih Acet committed
291

292
    onSearchInputBlur(e) {
Fatih Acet committed
293 294
      this.isFocused = false;
      this.wrap.removeClass('search-active');
295
      // If input is blank then restore state
Fatih Acet committed
296 297 298
      if (this.searchInput.val() === '') {
        return this.restoreOriginalState();
      }
299
    }
Fatih Acet committed
300

301
    addLocationBadge(item) {
Fatih Acet committed
302 303 304 305 306 307
      var badgeText, category, value;
      category = item.category != null ? item.category + ": " : '';
      value = item.value != null ? item.value : '';
      badgeText = "" + category + value;
      this.locationBadgeEl.text(badgeText).show();
      return this.wrap.addClass('has-location-badge');
308
    }
Fatih Acet committed
309

310
    hasLocationBadge() {
Fatih Acet committed
311
      return this.wrap.is('.has-location-badge');
312
    }
Fatih Acet committed
313

314
    restoreOriginalState() {
Fatih Acet committed
315 316
      var i, input, inputs, len;
      inputs = Object.keys(this.originalState);
317
      for (i = 0, len = inputs.length; i < len; i += 1) {
Fatih Acet committed
318 319 320 321 322 323 324 325 326 327
        input = inputs[i];
        this.getElement("#" + input).val(this.originalState[input]);
      }
      if (this.originalState._location === '') {
        return this.locationBadgeEl.hide();
      } else {
        return this.addLocationBadge({
          value: this.originalState._location
        });
      }
328
    }
Fatih Acet committed
329

330
    badgePresent() {
Fatih Acet committed
331
      return this.locationBadgeEl.length;
332
    }
Fatih Acet committed
333

334
    resetSearchState() {
Fatih Acet committed
335 336 337
      var i, input, inputs, len, results;
      inputs = Object.keys(this.originalState);
      results = [];
338
      for (i = 0, len = inputs.length; i < len; i += 1) {
Fatih Acet committed
339
        input = inputs[i];
340
        // _location isnt a input
Fatih Acet committed
341 342 343 344 345 346
        if (input === '_location') {
          break;
        }
        results.push(this.getElement("#" + input).val(''));
      }
      return results;
347
    }
Fatih Acet committed
348

349
    removeLocationBadge() {
Fatih Acet committed
350 351 352 353
      this.locationBadgeEl.hide();
      this.resetSearchState();
      this.wrap.removeClass('has-location-badge');
      return this.disableAutocomplete();
354
    }
Fatih Acet committed
355

356
    disableAutocomplete() {
357 358 359 360 361
      if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
        this.searchInput.addClass('disabled');
        this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
        this.restoreMenu();
      }
362
    }
Fatih Acet committed
363

364
    restoreMenu() {
Fatih Acet committed
365 366 367
      var html;
      html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
      return this.dropdownContent.html(html);
368
    }
Fatih Acet committed
369

370
    onClick(item, $el, e) {
Fatih Acet committed
371
      if (location.pathname.indexOf(item.url) !== -1) {
372
        if (!e.metaKey) e.preventDefault();
Fatih Acet committed
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
        if (!this.badgePresent) {
          if (item.category === 'Projects') {
            this.projectInputEl.val(item.id);
            this.addLocationBadge({
              value: 'This project'
            });
          }
          if (item.category === 'Groups') {
            this.groupInputEl.val(item.id);
            this.addLocationBadge({
              value: 'This group'
            });
          }
        }
        $el.removeClass('is-active');
        this.disableAutocomplete();
        return this.searchInput.val('').focus();
      }
391
    }
392
  }
Fatih Acet committed
393

394
  global.SearchAutocomplete = SearchAutocomplete;
Fatih Acet committed
395

396
  $(function() {
397 398 399
    var $projectOptionsDataEl = $('.js-search-project-options');
    var $groupOptionsDataEl = $('.js-search-group-options');
    var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
400

401
    if ($projectOptionsDataEl.length) {
402 403
      gl.projectOptions = gl.projectOptions || {};

404
      var projectPath = $projectOptionsDataEl.data('project-path');
405 406

      gl.projectOptions[projectPath] = {
407 408 409
        name: $projectOptionsDataEl.data('name'),
        issuesPath: $projectOptionsDataEl.data('issues-path'),
        mrPath: $projectOptionsDataEl.data('mr-path')
410 411
      };
    }
412 413

    if ($groupOptionsDataEl.length) {
414
      gl.groupOptions = gl.groupOptions || {};
415

416
      var groupPath = $groupOptionsDataEl.data('group-path');
417

418
      gl.groupOptions[groupPath] = {
419 420 421
        name: $groupOptionsDataEl.data('name'),
        issuesPath: $groupOptionsDataEl.data('issues-path'),
        mrPath: $groupOptionsDataEl.data('mr-path')
422 423
      };
    }
424

425
    if ($dashboardOptionsDataEl.length) {
426
      gl.dashboardOptions = {
427 428
        issuesPath: $dashboardOptionsDataEl.data('issues-path'),
        mrPath: $dashboardOptionsDataEl.data('mr-path')
429 430 431
      };
    }
  });
432
})(window.gl || (window.gl = {}));