debian-mirror-gitlab/app/assets/javascripts/search_autocomplete.js

517 lines
15 KiB
JavaScript
Raw Normal View History

2019-12-26 22:10:19 +05:30
/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */
2018-05-09 12:01:36 +05:30
import $ from 'jquery';
2020-07-28 23:09:34 +05:30
import { escape, throttle } from 'lodash';
2021-03-11 19:13:27 +05:30
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
2020-07-28 23:09:34 +05:30
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
2021-03-11 19:13:27 +05:30
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
2020-07-28 23:09:34 +05:30
import axios from './lib/utils/axios_utils';
2018-11-18 11:00:15 +05:30
import {
isInGroupsPage,
isInProjectPage,
getGroupSlug,
getProjectSlug,
spriteIcon,
} from './lib/utils/common_utils';
2018-03-17 18:26:18 +05:30
/**
* 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.
*/
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13,
UP: 38,
DOWN: 40,
};
function setSearchOptions() {
2019-12-26 22:10:19 +05:30
const $projectOptionsDataEl = $('.js-search-project-options');
const $groupOptionsDataEl = $('.js-search-group-options');
const $dashboardOptionsDataEl = $('.js-search-dashboard-options');
2018-03-17 18:26:18 +05:30
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
2019-12-26 22:10:19 +05:30
const projectPath = $projectOptionsDataEl.data('projectPath');
2018-03-17 18:26:18 +05:30
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
2018-03-27 19:54:05 +05:30
issuesPath: $projectOptionsDataEl.data('issuesPath'),
issuesDisabled: $projectOptionsDataEl.data('issuesDisabled'),
mrPath: $projectOptionsDataEl.data('mrPath'),
2018-03-17 18:26:18 +05:30
};
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
2016-09-13 17:45:13 +05:30
2019-12-26 22:10:19 +05:30
const groupPath = $groupOptionsDataEl.data('groupPath');
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
2018-03-27 19:54:05 +05:30
issuesPath: $groupOptionsDataEl.data('issuesPath'),
mrPath: $groupOptionsDataEl.data('mrPath'),
2018-03-17 18:26:18 +05:30
};
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
2018-11-18 11:00:15 +05:30
name: s__('SearchAutocomplete|All GitLab'),
2018-03-27 19:54:05 +05:30
issuesPath: $dashboardOptionsDataEl.data('issuesPath'),
mrPath: $dashboardOptionsDataEl.data('mrPath'),
2018-03-17 18:26:18 +05:30
};
}
}
2020-07-28 23:09:34 +05:30
export class SearchAutocomplete {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
2018-03-17 18:26:18 +05:30
setSearchOptions();
this.bindEventContext();
this.wrap = wrap || $('.search');
2020-07-28 23:09:34 +05:30
this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath');
2021-03-08 18:12:59 +05:30
this.projectId = projectId || this.optsEl.data('autocompleteProjectId') || '';
this.projectRef = projectRef || this.optsEl.data('autocompleteProjectRef') || '';
2018-03-17 18:26:18 +05:30
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
2018-11-18 11:00:15 +05:30
this.dropdownMenu = this.dropdown.find('.dropdown-menu');
2018-03-17 18:26:18 +05:30
this.dropdownContent = this.dropdown.find('.dropdown-content');
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');
2018-11-18 11:00:15 +05:30
this.scrollFadeInitialized = false;
2018-03-17 18:26:18 +05:30
this.saveOriginalState();
// Only when user is logged in
if (gon.current_user_id) {
2020-07-28 23:09:34 +05:30
this.createAutocomplete();
2016-11-03 12:29:30 +05:30
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
this.bindEvents();
this.dropdownToggle.dropdown();
2019-12-26 22:10:19 +05:30
this.searchInput.addClass('js-autocomplete-disabled');
2018-03-17 18:26:18 +05:30
}
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
// Finds an element inside wrapper element
bindEventContext() {
this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
this.onClearInputClick = this.onClearInputClick.bind(this);
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
2019-12-26 22:10:19 +05:30
this.onSearchInputChange = this.onSearchInputChange.bind(this);
2018-11-18 11:00:15 +05:30
this.setScrollFade = this.setScrollFade.bind(this);
2018-03-17 18:26:18 +05:30
}
getElement(selector) {
return this.wrap.find(selector);
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
saveOriginalState() {
2018-11-18 11:00:15 +05:30
return (this.originalState = this.serializeState());
2018-03-17 18:26:18 +05:30
}
2020-07-28 23:09:34 +05:30
createAutocomplete() {
2020-11-24 15:15:51 +05:30
return initDeprecatedJQueryDropdown(this.searchInput, {
2018-03-17 18:26:18 +05:30
filterInputBlur: false,
filterable: true,
filterRemote: true,
highlight: true,
2018-11-18 11:00:15 +05:30
icon: true,
2018-03-17 18:26:18 +05:30
enterCallback: false,
filterInput: 'input#search',
search: {
fields: ['text'],
},
id: this.getSearchText,
data: this.getData.bind(this),
selectable: true,
clicked: this.onClick.bind(this),
2020-11-24 15:15:51 +05:30
trackSuggestionClickedLabel: 'search_autocomplete_suggestion',
2018-03-17 18:26:18 +05:30
});
}
2019-12-04 20:38:33 +05:30
getSearchText(selectedObject) {
2018-03-17 18:26:18 +05:30
return selectedObject.id ? selectedObject.text : '';
}
getData(term, callback) {
if (!term) {
const contents = this.getCategoryContents();
if (contents) {
2020-11-24 15:15:51 +05:30
const deprecatedJQueryDropdownInstance = this.searchInput.data('deprecatedJQueryDropdown');
2018-11-18 11:00:15 +05:30
2020-11-24 15:15:51 +05:30
if (deprecatedJQueryDropdownInstance) {
deprecatedJQueryDropdownInstance.filter.options.callback(contents);
2018-11-18 11:00:15 +05:30
}
2020-07-28 23:09:34 +05:30
this.enableAutocomplete();
2016-09-13 17:45:13 +05:30
}
2018-03-17 18:26:18 +05:30
return;
2016-11-03 12:29:30 +05:30
}
2016-09-13 17:45:13 +05:30
2020-07-28 23:09:34 +05:30
// Prevent multiple ajax calls
if (this.loadingSuggestions) {
return;
}
2018-11-18 11:00:15 +05:30
2020-07-28 23:09:34 +05:30
this.loadingSuggestions = true;
return axios
.get(this.autocompletePath, {
params: {
project_id: this.projectId,
project_ref: this.projectRef,
term,
},
})
2021-03-08 18:12:59 +05:30
.then((response) => {
2020-07-28 23:09:34 +05:30
const options = this.scopedSearchOptions(term);
// List results
let lastCategory = null;
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) {
options.push({ type: 'separator' });
options.push({
type: 'header',
content: suggestion.category,
});
lastCategory = suggestion.category;
}
// Add the suggestion
options.push({
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
icon: this.getAvatar(suggestion),
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
});
}
2018-11-18 11:00:15 +05:30
2020-07-28 23:09:34 +05:30
callback(options);
this.loadingSuggestions = false;
this.highlightFirstRow();
this.setScrollFade();
})
.catch(() => {
this.loadingSuggestions = false;
});
}
getCategoryContents() {
const userName = gon.current_username;
const { projectOptions, groupOptions, dashboardOptions } = gl;
// Get options
let options;
if (isInProjectPage() && projectOptions) {
options = projectOptions[getProjectSlug()];
} else if (isInGroupsPage() && groupOptions) {
options = groupOptions[getGroupSlug()];
} else if (dashboardOptions) {
options = dashboardOptions;
}
const { issuesPath, mrPath, name, issuesDisabled } = options;
const baseItems = [];
if (name) {
baseItems.push({
type: 'header',
content: `${name}`,
});
}
const issueItems = [
{
text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_username=${userName}`,
},
{
text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_username=${userName}`,
},
];
const mergeRequestItems = [
{
text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_username=${userName}`,
},
2021-02-22 17:27:13 +05:30
{
text: s__("SearchAutocomplete|Merge requests that I'm a reviewer"),
url: `${mrPath}/?reviewer_username=${userName}`,
},
2020-07-28 23:09:34 +05:30
{
text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_username=${userName}`,
},
];
let items;
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
items = baseItems.concat(...issueItems, ...mergeRequestItems);
}
return items;
2018-03-17 18:26:18 +05:30
}
2016-09-13 17:45:13 +05:30
2020-06-23 00:09:42 +05:30
// Add option to proceed with the search for each
// scope that is currently available, namely:
//
// - Search in this project
// - Search in this group (or project's group)
// - Search in all GitLab
scopedSearchOptions(term) {
const icon = spriteIcon('search', 's16 inline-search-icon');
const projectId = this.projectInputEl.val();
const groupId = this.groupInputEl.val();
const options = [];
if (projectId) {
const projectOptions = gl.projectOptions[getProjectSlug()];
const url = groupId
? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}`
: `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}`;
options.push({
icon,
text: term,
template: sprintf(
s__(`SearchAutocomplete|in project %{projectName}`),
{
projectName: `<i>${projectOptions.name}</i>`,
},
false,
),
url,
});
2016-11-03 12:29:30 +05:30
}
2016-09-13 17:45:13 +05:30
2020-06-23 00:09:42 +05:30
if (groupId) {
const groupOptions = gl.groupOptions[getGroupSlug()];
options.push({
icon,
text: term,
template: sprintf(
s__(`SearchAutocomplete|in group %{groupName}`),
{
groupName: `<i>${groupOptions.name}</i>`,
},
false,
),
url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}`,
2018-03-17 18:26:18 +05:30
});
2016-11-03 12:29:30 +05:30
}
2016-09-13 17:45:13 +05:30
2020-06-23 00:09:42 +05:30
options.push({
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
url: `${gon.relative_url_root}/search?search=${term}`,
});
2018-03-17 18:26:18 +05:30
2020-06-23 00:09:42 +05:30
return options;
2018-03-17 18:26:18 +05:30
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
serializeState() {
return {
// Search Criteria
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(),
};
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
bindEvents() {
2019-12-26 22:10:19 +05:30
this.searchInput.on('input', this.onSearchInputChange);
2018-03-17 18:26:18 +05:30
this.searchInput.on('keyup', this.onSearchInputKeyUp);
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
2018-11-18 11:00:15 +05:30
this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
2019-12-26 22:10:19 +05:30
2021-03-08 18:12:59 +05:30
this.searchInput.on('click', (e) => {
2019-12-26 22:10:19 +05:30
e.stopPropagation();
});
2018-03-17 18:26:18 +05:30
}
2020-07-28 23:09:34 +05:30
enableAutocomplete() {
2018-11-18 11:00:15 +05:30
this.setScrollFade();
2018-03-17 18:26:18 +05:30
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
2016-11-03 12:29:30 +05:30
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
// If the dropdown is closed, we'll open it
2018-11-08 19:23:39 +05:30
if (!this.dropdown.hasClass('show')) {
2018-03-17 18:26:18 +05:30
this.loadingSuggestions = false;
this.dropdownToggle.dropdown('toggle');
2020-10-24 23:57:45 +05:30
const trackEvent = 'click_search_bar';
const trackCategory = undefined; // will be default set in event method
Tracking.event(trackCategory, trackEvent, {
label: 'main_navigation',
property: 'navigation',
});
2019-12-26 22:10:19 +05:30
return this.searchInput.removeClass('js-autocomplete-disabled');
2016-11-03 12:29:30 +05:30
}
2018-03-17 18:26:18 +05:30
}
2016-09-13 17:45:13 +05:30
2019-12-26 22:10:19 +05:30
onSearchInputChange() {
2020-07-28 23:09:34 +05:30
this.enableAutocomplete();
2018-03-17 18:26:18 +05:30
}
onSearchInputKeyUp(e) {
switch (e.keyCode) {
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
2020-07-28 23:09:34 +05:30
this.disableAutocomplete();
2018-03-17 18:26:18 +05:30
break;
default:
2016-11-03 12:29:30 +05:30
}
2019-09-04 21:01:54 +05:30
this.wrap.toggleClass('has-value', Boolean(e.target.value));
2018-03-17 18:26:18 +05:30
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
onSearchInputFocus() {
this.isFocused = true;
this.wrap.addClass('search-active');
if (this.getValue() === '') {
return this.getData();
2016-11-03 12:29:30 +05:30
}
2018-03-17 18:26:18 +05:30
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
getValue() {
return this.searchInput.val();
}
2016-09-13 17:45:13 +05:30
2018-03-17 18:26:18 +05:30
onClearInputClick(e) {
e.preventDefault();
2019-09-04 21:01:54 +05:30
this.wrap.toggleClass('has-value', Boolean(e.target.value));
2018-03-17 18:26:18 +05:30
return this.searchInput.val('').focus();
}
2016-09-13 17:45:13 +05:30
2019-12-04 20:38:33 +05:30
onSearchInputBlur() {
2018-03-17 18:26:18 +05:30
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
2019-09-04 21:01:54 +05:30
this.restoreOriginalState();
2017-08-17 22:00:37 +05:30
}
2019-09-04 21:01:54 +05:30
this.dropdownMenu.removeClass('show');
2016-11-03 12:29:30 +05:30
}
2018-03-17 18:26:18 +05:30
restoreOriginalState() {
2019-12-26 22:10:19 +05:30
const inputs = Object.keys(this.originalState);
for (let i = 0, len = inputs.length; i < len; i += 1) {
const input = inputs[i];
2019-12-21 20:55:43 +05:30
this.getElement(`#${input}`).val(this.originalState[input]);
2018-03-17 18:26:18 +05:30
}
}
2016-11-03 12:29:30 +05:30
2018-03-17 18:26:18 +05:30
resetSearchState() {
2019-12-26 22:10:19 +05:30
const inputs = Object.keys(this.originalState);
const results = [];
for (let i = 0, len = inputs.length; i < len; i += 1) {
const input = inputs[i];
2019-12-21 20:55:43 +05:30
results.push(this.getElement(`#${input}`).val(''));
2016-11-03 12:29:30 +05:30
}
2018-03-17 18:26:18 +05:30
return results;
}
2016-09-13 17:45:13 +05:30
2020-07-28 23:09:34 +05:30
disableAutocomplete() {
2019-12-26 22:10:19 +05:30
if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('js-autocomplete-disabled');
2020-05-24 23:13:21 +05:30
this.dropdownToggle.dropdown('toggle');
2018-03-17 18:26:18 +05:30
this.restoreMenu();
2016-11-03 12:29:30 +05:30
}
2018-03-17 18:26:18 +05:30
}
restoreMenu() {
2019-12-26 22:10:19 +05:30
const html = `<ul><li class="dropdown-menu-empty-item"><a>${__('Loading...')}</a></li></ul>`;
2018-03-17 18:26:18 +05:30
return this.dropdownContent.html(html);
}
2016-11-03 12:29:30 +05:30
2018-03-17 18:26:18 +05:30
onClick(item, $el, e) {
2018-11-08 19:23:39 +05:30
if (window.location.pathname.indexOf(item.url) !== -1) {
2018-03-17 18:26:18 +05:30
if (!e.metaKey) e.preventDefault();
2020-07-28 23:09:34 +05:30
/* eslint-disable-next-line @gitlab/require-i18n-strings */
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
}
// eslint-disable-next-line @gitlab/require-i18n-strings
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
}
2018-03-17 18:26:18 +05:30
$el.removeClass('is-active');
2020-07-28 23:09:34 +05:30
this.disableAutocomplete();
2018-03-17 18:26:18 +05:30
return this.searchInput.val('').focus();
2016-11-03 12:29:30 +05:30
}
2018-03-17 18:26:18 +05:30
}
2018-11-18 11:00:15 +05:30
highlightFirstRow() {
2020-11-24 15:15:51 +05:30
this.searchInput.data('deprecatedJQueryDropdown').highlightRowAtIndex(null, 0);
2018-11-18 11:00:15 +05:30
}
2020-07-28 23:09:34 +05:30
getAvatar(item) {
if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
return false;
2020-06-23 00:09:42 +05:30
}
2020-07-28 23:09:34 +05:30
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>`;
2018-11-18 11:00:15 +05:30
2020-07-28 23:09:34 +05:30
return avatar;
2018-11-18 11:00:15 +05:30
}
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());
}
2018-03-17 18:26:18 +05:30
}
2018-12-05 23:21:45 +05:30
2020-07-28 23:09:34 +05:30
export default function initSearchAutocomplete(opts) {
return new SearchAutocomplete(opts);
2018-12-05 23:21:45 +05:30
}