diff options
| author | Annabel Dunstone Gray <annabel.dunstone@gmail.com> | 2017-12-06 19:53:05 +0000 | 
|---|---|---|
| committer | Annabel Dunstone Gray <annabel.dunstone@gmail.com> | 2017-12-06 19:53:05 +0000 | 
| commit | 743451b231445bfc2ee295d8097bdcdeb1ff47ad (patch) | |
| tree | 092a7a8c0171bb77b72ef9bcaba46d5585262e7d | |
| parent | 6808d11b058939e61deea8c164f5b01aa19c9176 (diff) | |
| parent | 67f20699d5c860f4cbdaeada86032c24a91bd9b2 (diff) | |
| download | gitlab-ce-743451b231445bfc2ee295d8097bdcdeb1ff47ad.tar.gz | |
Merge branch '35773-search-box-close-dropdown' into 'master'
Close all open dropdowns when search input is clicked
Closes #35773
See merge request gitlab-org/gitlab-ce!15737
| -rw-r--r-- | app/assets/javascripts/search_autocomplete.js | 141 | ||||
| -rw-r--r-- | app/views/layouts/_search.html.haml | 3 | ||||
| -rw-r--r-- | spec/javascripts/search_autocomplete_spec.js | 2 | 
3 files changed, 77 insertions, 69 deletions
| diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 9dec5d7645a..e40a3596200 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,13 +1,20 @@ -/* 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 */ +/* eslint-disable 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-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */  import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; +/** + * Search input in top navigation bar. + * On click, opens a dropdown + * As the user types it filters the results + * When the user clicks `x` button it cleans the input and closes the dropdown. + */ +  ((global) => {    const KEYCODE = {      ESCAPE: 27,      BACKSPACE: 8,      ENTER: 13,      UP: 38, -    DOWN: 40 +    DOWN: 40,    };    class SearchAutocomplete { @@ -19,6 +26,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');        this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');        this.dropdown = this.wrap.find('.dropdown'); +      this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');        this.dropdownContent = this.dropdown.find('.dropdown-content');        this.locationBadgeEl = this.getElement('.location-badge');        this.scopeInputEl = this.getElement('#scope'); @@ -29,13 +37,16 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        this.repositoryInputEl = this.getElement('#repository_ref');        this.clearInput = this.getElement('.js-clear-input');        this.saveOriginalState(); +        // Only when user is logged in        if (gon.current_user_id) {          this.createAutocomplete();        } +        this.searchInput.addClass('disabled');        this.saveTextLength();        this.bindEvents(); +      this.dropdownToggle.dropdown();      }      // Finds an element inside wrapper element @@ -43,7 +54,6 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        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);      } @@ -68,12 +78,12 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.          enterCallback: false,          filterInput: 'input#search',          search: { -          fields: ['text'] +          fields: ['text'],          },          id: this.getSearchText,          data: this.getData.bind(this),          selectable: true, -        clicked: this.onClick.bind(this) +        clicked: this.onClick.bind(this),        });      } @@ -82,32 +92,35 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.      }      getData(term, callback) { -      var _this, contents, jqXHR; -      _this = this;        if (!term) { -        if (contents = this.getCategoryContents()) { +        const contents = this.getCategoryContents(); +        if (contents) {            this.searchInput.data('glDropdown').filter.options.callback(contents);            this.enableAutocomplete();          }          return;        } +        // Prevent multiple ajax calls        if (this.loadingSuggestions) {          return;        } +        this.loadingSuggestions = true; -      return jqXHR = $.get(this.autocompletePath, { + +      return $.get(this.autocompletePath, {          project_id: this.projectId,          project_ref: this.projectRef, -        term: term -      }, function(response) { -        var data, firstCategory, i, lastCategory, len, suggestion; +        term: term, +      }, (response) => { +        var firstCategory, i, lastCategory, len, suggestion;          // Hide dropdown menu if no suggestions returns          if (!response.length) { -          _this.disableAutocomplete(); +          this.disableAutocomplete();            return;          } -        data = []; + +        const data = [];          // List results          firstCategory = true;          for (i = 0, len = response.length; i < len; i += 1) { @@ -121,7 +134,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.                firstCategory = false;              }              data.push({ -              header: suggestion.category +              header: suggestion.category,              });              lastCategory = suggestion.category;            } @@ -129,7 +142,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.              id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,              category: suggestion.category,              text: suggestion.label, -            url: suggestion.url +            url: suggestion.url,            });          }          // Add option to proceed with the search @@ -137,20 +150,21 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.            data.push('separator');            data.push({              text: "Result name contains \"" + term + "\"", -            url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val()) +            url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()),            });          }          return callback(data); -      }).always(function() { -        return _this.loadingSuggestions = false; -      }); +      }) +      .always(() => { this.loadingSuggestions = false; });      }      getCategoryContents() { -      var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName; -      userId = gon.current_user_id; -      userName = gon.current_username; -      projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; +      const userId = gon.current_user_id; +      const userName = gon.current_username; +      const { projectOptions, groupOptions, dashboardOptions } = gl; + +      // Get options +      let options;        if (isInGroupsPage() && groupOptions) {          options = groupOptions[getGroupSlug()];        } else if (isInProjectPage() && projectOptions) { @@ -158,37 +172,42 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        } else if (dashboardOptions) {          options = dashboardOptions;        } -      issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name; -      items = [ -        { -          header: "" + name -        } -      ]; + +      const { issuesPath, mrPath, name, issuesDisabled } = options; +      const baseItems = []; + +      if (name) { +        baseItems.push({ +          header: `${name}`, +        }); +      } +        const issueItems = [          {            text: 'Issues assigned to me', -          url: issuesPath + "/?assignee_username=" + userName -        }, { +          url: `${issuesPath}/?assignee_username=${userName}`, +        }, +        {            text: "Issues I've created", -          url: issuesPath + "/?author_username=" + userName -        } +          url: `${issuesPath}/?author_username=${userName}`, +        },        ];        const mergeRequestItems = [          {            text: 'Merge requests assigned to me', -          url: mrPath + "/?assignee_username=" + userName -        }, { +          url: `${mrPath}/?assignee_username=${userName}`, +        }, +        {            text: "Merge requests I've created", -          url: mrPath + "/?author_username=" + userName -        } +          url: `${mrPath}/?author_username=${userName}`, +        },        ]; -      if (options.issuesDisabled) { -        items = items.concat(mergeRequestItems); + +      let items; +      if (issuesDisabled) { +        items = baseItems.concat(mergeRequestItems);        } else { -        items = items.concat(...issueItems, 'separator', ...mergeRequestItems); -      } -      if (!name) { -        items.splice(0, 1); +        items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);        }        return items;      } @@ -202,39 +221,34 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.          repository_ref: this.repositoryInputEl.val(),          scope: this.scopeInputEl.val(),          // Location badge -        _location: this.locationBadgeEl.text() +        _location: this.locationBadgeEl.text(),        };      }      bindEvents() {        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)); +      this.locationBadgeEl.on('click', () => this.searchInput.focus());      }      enableAutocomplete() { -      var _this;        // No need to enable anything if user is not logged in        if (!gon.current_user_id) {          return;        } + +      // If the dropdown is closed, we'll open it        if (!this.dropdown.hasClass('open')) { -        _this = this;          this.loadingSuggestions = false; -        this.dropdown.addClass('open').trigger('shown.bs.dropdown'); +        this.dropdownToggle.dropdown('toggle');          return this.searchInput.removeClass('disabled');        }      } -      // Saves last length of the entered text +    // Saves last length of the entered text      onSearchInputKeyDown() {        return this.saveTextLength();      } @@ -279,11 +293,6 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        this.wrap.toggleClass('has-value', !!e.target.value);      } -    // Avoid falsy value to be returned -    onSearchInputClick(e) { -      return e.stopImmediatePropagation(); -    } -      onSearchInputFocus() {        this.isFocused = true;        this.wrap.addClass('search-active'); @@ -335,7 +344,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.          return this.locationBadgeEl.hide();        } else {          return this.addLocationBadge({ -          value: this.originalState._location +          value: this.originalState._location,          });        }      } @@ -387,13 +396,13 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.            if (item.category === 'Projects') {              this.projectInputEl.val(item.id);              this.addLocationBadge({ -              value: 'This project' +              value: 'This project',              });            }            if (item.category === 'Groups') {              this.groupInputEl.val(item.id);              this.addLocationBadge({ -              value: 'This group' +              value: 'This group',              });            }          } @@ -420,7 +429,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.          name: $projectOptionsDataEl.data('name'),          issuesPath: $projectOptionsDataEl.data('issues-path'),          issuesDisabled: $projectOptionsDataEl.data('issues-disabled'), -        mrPath: $projectOptionsDataEl.data('mr-path') +        mrPath: $projectOptionsDataEl.data('mr-path'),        };      } @@ -432,14 +441,14 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.        gl.groupOptions[groupPath] = {          name: $groupOptionsDataEl.data('name'),          issuesPath: $groupOptionsDataEl.data('issues-path'), -        mrPath: $groupOptionsDataEl.data('mr-path') +        mrPath: $groupOptionsDataEl.data('mr-path'),        };      }      if ($dashboardOptionsDataEl.length) {        gl.dashboardOptions = {          issuesPath: $dashboardOptionsDataEl.data('issues-path'), -        mrPath: $dashboardOptionsDataEl.data('mr-path') +        mrPath: $dashboardOptionsDataEl.data('mr-path'),        };      }    }); diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 4c5cc249159..30ae385f62f 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -13,7 +13,8 @@          .location-badge= label        .search-input-wrap          .dropdown{ data: { url: search_autocomplete_path } } -          = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' } +          = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' } +          %button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }            .dropdown-menu.dropdown-select              = dropdown_content do                %ul diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index a2394857b82..fdfc59a6f12 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -191,8 +191,6 @@ import '~/lib/utils/common_utils';        // browsers will not trigger default behavior (form submit, in this        // example) on JavaScript-created keypresses.        expect(submitSpy).not.toHaveBeenTriggered(); -      // Does a worse job at capturing the intent of the test, but works. -      expect(enterKeyEvent.isDefaultPrevented()).toBe(true);      });    });  }).call(window); | 
