summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/search_autocomplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/search_autocomplete.js')
-rw-r--r--app/assets/javascripts/search_autocomplete.js263
1 files changed, 149 insertions, 114 deletions
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 72a2c7ca101..aec09b8bc0a 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,9 +1,18 @@
-/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, quotes, class-methods-use-this, no-lonely-if, no-else-return, vars-on-top, max-len */
+/* eslint-disable no-return-assign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top, max-len */
import $ from 'jquery';
+import { escape, throttle } from 'underscore';
+import { s__, sprintf } from '~/locale';
+import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import DropdownUtils from './filtered_search/dropdown_utils';
-import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
+import {
+ isInGroupsPage,
+ isInProjectPage,
+ getGroupSlug,
+ getProjectSlug,
+ spriteIcon,
+} from './lib/utils/common_utils';
/**
* Search input in top navigation bar.
@@ -52,6 +61,7 @@ function setSearchOptions() {
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
+ name: s__('SearchAutocomplete|All GitLab'),
issuesPath: $dashboardOptionsDataEl.data('issuesPath'),
mrPath: $dashboardOptionsDataEl.data('mrPath'),
};
@@ -69,8 +79,8 @@ export default class SearchAutocomplete {
this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
+ this.dropdownMenu = this.dropdown.find('.dropdown-menu');
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');
@@ -78,6 +88,7 @@ export default class SearchAutocomplete {
this.searchCodeInputEl = this.getElement('#search_code');
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
+ this.scrollFadeInitialized = false;
this.saveOriginalState();
// Only when user is logged in
@@ -98,17 +109,18 @@ export default class SearchAutocomplete {
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+ this.setScrollFade = this.setScrollFade.bind(this);
}
getElement(selector) {
return this.wrap.find(selector);
}
saveOriginalState() {
- return this.originalState = this.serializeState();
+ return (this.originalState = this.serializeState());
}
saveTextLength() {
- return this.lastTextLength = this.searchInput.val().length;
+ return (this.lastTextLength = this.searchInput.val().length);
}
createAutocomplete() {
@@ -117,6 +129,7 @@ export default class SearchAutocomplete {
filterable: true,
filterRemote: true,
highlight: true,
+ icon: true,
enterCallback: false,
filterInput: 'input#search',
search: {
@@ -154,60 +167,87 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true;
- return axios.get(this.autocompletePath, {
- params: {
- project_id: this.projectId,
- project_ref: this.projectRef,
- term: term,
- },
- }).then((response) => {
- // Hide dropdown menu if no suggestions returns
- if (!response.data.length) {
- this.disableAutocomplete();
- return;
- }
+ return axios
+ .get(this.autocompletePath, {
+ params: {
+ project_id: this.projectId,
+ project_ref: this.projectRef,
+ term: term,
+ },
+ })
+ .then(response => {
+ // Hide dropdown menu if no suggestions returns
+ if (!response.data.length) {
+ this.disableAutocomplete();
+ return;
+ }
- const data = [];
- // List results
- let firstCategory = true;
- let lastCategory;
- for (let i = 0, len = response.data.length; i < len; i += 1) {
- const suggestion = response.data[i];
- // Add group header before list each group
- if (lastCategory !== suggestion.category) {
- if (!firstCategory) {
- data.push('separator');
- }
- if (firstCategory) {
- firstCategory = false;
+ const data = [];
+ // List results
+ let firstCategory = true;
+ let lastCategory;
+ for (let i = 0, len = response.data.length; i < len; i += 1) {
+ const suggestion = response.data[i];
+ // Add group header before list each group
+ if (lastCategory !== suggestion.category) {
+ if (!firstCategory) {
+ data.push('separator');
+ }
+ if (firstCategory) {
+ firstCategory = false;
+ }
+ data.push({
+ header: suggestion.category,
+ });
+ lastCategory = suggestion.category;
}
data.push({
- header: suggestion.category,
+ id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
+ icon: this.getAvatar(suggestion),
+ category: suggestion.category,
+ text: suggestion.label,
+ url: suggestion.url,
});
- lastCategory = suggestion.category;
}
- data.push({
- id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
- category: suggestion.category,
- text: suggestion.label,
- url: suggestion.url,
- });
- }
- // Add option to proceed with the search
- 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()}`,
- });
- }
+ // Add option to proceed with the search
+ if (data.length) {
+ const icon = spriteIcon('search', 's16 inline-search-icon');
+ let template;
- callback(data);
+ if (this.projectInputEl.val()) {
+ template = s__('SearchAutocomplete|in this project');
+ }
+ if (this.groupInputEl.val()) {
+ template = s__('SearchAutocomplete|in this group');
+ }
- this.loadingSuggestions = false;
- }).catch(() => {
- this.loadingSuggestions = false;
- });
+ data.unshift('separator');
+ data.unshift({
+ icon,
+ text: term,
+ template: s__('SearchAutocomplete|in all GitLab'),
+ url: `/search?search=${term}`,
+ });
+
+ if (template) {
+ data.unshift({
+ icon,
+ text: term,
+ template,
+ url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
+ });
+ }
+ }
+
+ callback(data);
+
+ this.loadingSuggestions = false;
+ this.highlightFirstRow();
+ this.setScrollFade();
+ })
+ .catch(() => {
+ this.loadingSuggestions = false;
+ });
}
getCategoryContents() {
@@ -236,21 +276,21 @@ export default class SearchAutocomplete {
const issueItems = [
{
- text: 'Issues assigned to me',
+ text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_id=${userId}`,
},
{
- text: "Issues I've created",
+ text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_id=${userId}`,
},
];
const mergeRequestItems = [
{
- text: 'Merge requests assigned to me',
+ text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_id=${userId}`,
},
{
- text: "Merge requests I've created",
+ text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_id=${userId}`,
},
];
@@ -259,7 +299,7 @@ export default class SearchAutocomplete {
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
- items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
+ items = baseItems.concat(...issueItems, ...mergeRequestItems);
}
return items;
}
@@ -272,8 +312,6 @@ export default class SearchAutocomplete {
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
- // Location badge
- _location: this.locationBadgeEl.text(),
};
}
@@ -283,10 +321,12 @@ export default class SearchAutocomplete {
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
- this.locationBadgeEl.on('click', () => this.searchInput.focus());
+ this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
}
enableAutocomplete() {
+ this.setScrollFade();
+
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
@@ -308,10 +348,6 @@ export default class SearchAutocomplete {
onSearchInputKeyUp(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
- // when trying to remove the location badge
- if (this.lastTextLength === 0 && this.badgePresent()) {
- this.removeLocationBadge();
- }
// When removing the last character and no badge is present
if (this.lastTextLength === 1) {
this.disableAutocomplete();
@@ -372,37 +408,13 @@ export default class SearchAutocomplete {
}
}
- addLocationBadge(item) {
- 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');
- }
-
- hasLocationBadge() {
- return this.wrap.is('.has-location-badge');
- }
-
restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
- this.getElement("#" + input).val(this.originalState[input]);
+ this.getElement('#' + input).val(this.originalState[input]);
}
- if (this.originalState._location === '') {
- return this.locationBadgeEl.hide();
- } else {
- return this.addLocationBadge({
- value: this.originalState._location,
- });
- }
- }
-
- badgePresent() {
- return this.locationBadgeEl.length;
}
resetSearchState() {
@@ -411,22 +423,11 @@ export default class SearchAutocomplete {
results = [];
for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
- // _location isnt a input
- if (input === '_location') {
- break;
- }
- results.push(this.getElement("#" + input).val(''));
+ results.push(this.getElement('#' + input).val(''));
}
return results;
}
- removeLocationBadge() {
- this.locationBadgeEl.hide();
- this.resetSearchState();
- this.wrap.removeClass('has-location-badge');
- return this.disableAutocomplete();
- }
-
disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('disabled');
@@ -444,23 +445,57 @@ export default class SearchAutocomplete {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
- 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',
- });
- }
+ if (item.category === 'Projects') {
+ this.projectInputEl.val(item.id);
+ }
+ if (item.category === 'Groups') {
+ this.groupInputEl.val(item.id);
}
$el.removeClass('is-active');
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
}
+
+ highlightFirstRow() {
+ this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
+ }
+
+ getAvatar(item) {
+ if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
+ return false;
+ }
+
+ const { label, id } = item;
+ const avatarUrl = item.avatar_url;
+ const avatar = avatarUrl
+ ? `<img class="search-item-avatar" src="${avatarUrl}" />`
+ : `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
+ escape(label),
+ )}</div>`;
+
+ return avatar;
+ }
+
+ isScrolledUp() {
+ const el = this.dropdownContent[0];
+ const currentPosition = this.contentClientHeight + el.scrollTop;
+
+ return currentPosition < this.maxPosition;
+ }
+
+ initScrollFade() {
+ const el = this.dropdownContent[0];
+ this.scrollFadeInitialized = true;
+
+ this.contentClientHeight = el.clientHeight;
+ this.maxPosition = el.scrollHeight;
+ this.dropdownMenu.addClass('dropdown-content-faded-mask');
+ }
+
+ setScrollFade() {
+ this.initScrollFade();
+
+ this.dropdownMenu.toggleClass('fade-out', !this.isScrolledUp());
+ }
}