2017-03-13 11:46:43 +00:00
|
|
|
import FilteredSearchContainer from './container';
|
2017-03-30 02:04:05 +00:00
|
|
|
import RecentSearchesRoot from './recent_searches_root';
|
|
|
|
import RecentSearchesStore from './stores/recent_searches_store';
|
|
|
|
import RecentSearchesService from './services/recent_searches_service';
|
|
|
|
import eventHub from './event_hub';
|
2017-06-27 18:37:30 +00:00
|
|
|
import { addClassIfElementExists } from '../lib/utils/dom_utils';
|
2017-03-10 17:40:38 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
class FilteredSearchManager {
|
|
|
|
constructor(page) {
|
2017-05-22 19:58:23 +00:00
|
|
|
this.page = page;
|
2017-04-17 07:01:11 +00:00
|
|
|
this.container = FilteredSearchContainer.container;
|
|
|
|
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
|
|
|
this.filteredSearchInputForm = this.filteredSearchInput.form;
|
|
|
|
this.clearSearchButton = this.container.querySelector('.clear-search');
|
|
|
|
this.tokensContainer = this.container.querySelector('.tokens-container');
|
|
|
|
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
|
|
|
|
|
2017-05-05 17:59:41 +00:00
|
|
|
this.recentSearchesStore = new RecentSearchesStore({
|
|
|
|
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
|
2017-05-23 16:35:54 +00:00
|
|
|
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
|
2017-05-05 17:59:41 +00:00
|
|
|
});
|
2017-05-22 19:58:23 +00:00
|
|
|
this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
|
2017-08-02 09:27:24 +00:00
|
|
|
const fullPath = this.searchHistoryDropdownElement ?
|
|
|
|
this.searchHistoryDropdownElement.dataset.fullPath : 'project';
|
2017-05-06 02:33:21 +00:00
|
|
|
let recentSearchesPagePrefix = 'issue-recent-searches';
|
2017-05-22 19:58:23 +00:00
|
|
|
if (this.page === 'merge_requests') {
|
2017-05-06 02:33:21 +00:00
|
|
|
recentSearchesPagePrefix = 'merge-request-recent-searches';
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-08-02 09:27:24 +00:00
|
|
|
const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`;
|
2017-04-17 07:01:11 +00:00
|
|
|
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
|
2017-05-22 19:58:23 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
|
2017-05-22 19:58:23 +00:00
|
|
|
setup() {
|
2017-04-17 07:01:11 +00:00
|
|
|
// Fetch recent searches from localStorage
|
|
|
|
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
|
2017-05-05 17:59:41 +00:00
|
|
|
.catch((error) => {
|
|
|
|
if (error.name === 'RecentSearchesServiceError') return undefined;
|
2017-04-17 07:01:11 +00:00
|
|
|
// eslint-disable-next-line no-new
|
2017-09-25 20:02:08 +00:00
|
|
|
new window.Flash('An error occurred while parsing recent searches');
|
2017-04-17 07:01:11 +00:00
|
|
|
// Gracefully fail to empty array
|
|
|
|
return [];
|
|
|
|
})
|
|
|
|
.then((searches) => {
|
2017-06-23 09:28:19 +00:00
|
|
|
if (!searches) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
// Put any searches that may have come in before
|
|
|
|
// we fetched the saved searches ahead of the already saved ones
|
|
|
|
const resultantSearches = this.recentSearchesStore.setRecentSearches(
|
|
|
|
this.recentSearchesStore.state.recentSearches.concat(searches),
|
2017-03-30 02:04:05 +00:00
|
|
|
);
|
2017-04-17 07:01:11 +00:00
|
|
|
this.recentSearchesService.save(resultantSearches);
|
|
|
|
});
|
2017-03-30 02:04:05 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (this.filteredSearchInput) {
|
|
|
|
this.tokenizer = gl.FilteredSearchTokenizer;
|
2017-05-22 19:58:23 +00:00
|
|
|
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, this.page);
|
2017-01-09 18:15:10 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.recentSearchesRoot = new RecentSearchesRoot(
|
|
|
|
this.recentSearchesStore,
|
|
|
|
this.recentSearchesService,
|
2017-05-22 19:58:23 +00:00
|
|
|
this.searchHistoryDropdownElement,
|
2017-04-17 07:01:11 +00:00
|
|
|
);
|
|
|
|
this.recentSearchesRoot.init();
|
2016-12-08 21:42:58 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.bindEvents();
|
|
|
|
this.loadSearchParamsFromURL();
|
|
|
|
this.dropdownManager.setDropdown();
|
2017-03-30 02:04:05 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.cleanupWrapper = this.cleanup.bind(this);
|
|
|
|
document.addEventListener('beforeunload', this.cleanupWrapper);
|
2016-12-09 17:44:09 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2016-12-08 21:42:58 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
cleanup() {
|
|
|
|
this.unbindEvents();
|
|
|
|
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
2016-12-09 21:36:44 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (this.recentSearchesRoot) {
|
|
|
|
this.recentSearchesRoot.destroy();
|
2016-12-07 18:55:03 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2016-12-07 18:55:03 +00:00
|
|
|
|
2017-06-06 01:06:38 +00:00
|
|
|
bindStateEvents() {
|
2017-06-06 14:58:59 +00:00
|
|
|
this.stateFilters = document.querySelector('.container-fluid .issues-state-filters');
|
|
|
|
|
|
|
|
if (this.stateFilters) {
|
|
|
|
this.searchStateWrapper = this.searchState.bind(this);
|
|
|
|
|
2017-06-07 15:54:54 +00:00
|
|
|
this.stateFilters.querySelector('[data-state="opened"]')
|
2017-06-06 14:58:59 +00:00
|
|
|
.addEventListener('click', this.searchStateWrapper);
|
2017-06-07 15:54:54 +00:00
|
|
|
this.stateFilters.querySelector('[data-state="closed"]')
|
2017-06-06 14:58:59 +00:00
|
|
|
.addEventListener('click', this.searchStateWrapper);
|
2017-06-07 15:54:54 +00:00
|
|
|
this.stateFilters.querySelector('[data-state="all"]')
|
2017-06-06 14:58:59 +00:00
|
|
|
.addEventListener('click', this.searchStateWrapper);
|
|
|
|
|
2017-06-07 15:54:54 +00:00
|
|
|
this.mergedState = this.stateFilters.querySelector('[data-state="merged"]');
|
2017-06-06 14:58:59 +00:00
|
|
|
if (this.mergedState) {
|
|
|
|
this.mergedState.addEventListener('click', this.searchStateWrapper);
|
2017-06-06 01:06:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unbindStateEvents() {
|
2017-06-06 14:58:59 +00:00
|
|
|
if (this.stateFilters) {
|
2017-06-07 15:54:54 +00:00
|
|
|
this.stateFilters.querySelector('[data-state="opened"]')
|
2017-06-06 14:58:59 +00:00
|
|
|
.removeEventListener('click', this.searchStateWrapper);
|
2017-06-07 15:54:54 +00:00
|
|
|
this.stateFilters.querySelector('[data-state="closed"]')
|
2017-06-06 14:58:59 +00:00
|
|
|
.removeEventListener('click', this.searchStateWrapper);
|
2017-06-07 15:54:54 +00:00
|
|
|
this.stateFilters.querySelector('[data-state="all"]')
|
2017-06-06 14:58:59 +00:00
|
|
|
.removeEventListener('click', this.searchStateWrapper);
|
|
|
|
|
|
|
|
if (this.mergedState) {
|
|
|
|
this.mergedState.removeEventListener('click', this.searchStateWrapper);
|
2017-06-06 01:06:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
bindEvents() {
|
|
|
|
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
|
|
|
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
|
|
|
|
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
|
|
|
|
this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
|
|
|
|
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
|
|
|
|
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
|
|
|
this.onClearSearchWrapper = this.onClearSearch.bind(this);
|
|
|
|
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
|
2017-04-26 20:12:33 +00:00
|
|
|
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
|
2017-04-17 07:01:11 +00:00
|
|
|
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
|
|
|
|
this.editTokenWrapper = this.editToken.bind(this);
|
|
|
|
this.tokenChange = this.tokenChange.bind(this);
|
|
|
|
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
|
|
|
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
|
|
|
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
2017-04-26 20:12:33 +00:00
|
|
|
this.removeTokenWrapper = this.removeToken.bind(this);
|
2017-04-17 07:01:11 +00:00
|
|
|
|
|
|
|
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
|
|
|
|
this.filteredSearchInput.addEventListener('click', this.tokenChange);
|
|
|
|
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
|
|
|
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
2017-04-26 20:12:33 +00:00
|
|
|
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
|
2017-06-06 15:48:45 +00:00
|
|
|
this.tokensContainer.addEventListener('click', this.editTokenWrapper);
|
2017-04-17 07:01:11 +00:00
|
|
|
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
|
|
|
document.addEventListener('click', this.unselectEditTokensWrapper);
|
|
|
|
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
2017-04-26 20:12:33 +00:00
|
|
|
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
2017-04-17 07:01:11 +00:00
|
|
|
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
2017-06-06 01:06:38 +00:00
|
|
|
|
|
|
|
this.bindStateEvents();
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
unbindEvents() {
|
|
|
|
this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
|
|
|
|
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
|
|
|
|
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
|
|
|
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
2017-04-26 20:12:33 +00:00
|
|
|
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
|
2017-06-06 15:48:45 +00:00
|
|
|
this.tokensContainer.removeEventListener('click', this.editTokenWrapper);
|
2017-04-17 07:01:11 +00:00
|
|
|
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
|
|
|
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
|
|
|
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
2017-04-26 20:12:33 +00:00
|
|
|
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
2017-04-17 07:01:11 +00:00
|
|
|
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
2017-06-06 01:06:38 +00:00
|
|
|
|
|
|
|
this.unbindStateEvents();
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
checkForBackspace(e) {
|
|
|
|
// 8 = Backspace Key
|
|
|
|
// 46 = Delete Key
|
|
|
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
|
|
|
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-05-22 19:58:23 +00:00
|
|
|
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
|
|
|
|
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
|
|
|
|
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
|
2017-04-17 07:01:11 +00:00
|
|
|
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
|
|
|
|
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
|
2016-12-09 21:36:44 +00:00
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
// Reposition dropdown so that it is aligned with cursor
|
|
|
|
this.dropdownManager.updateCurrentDropdownOffset();
|
|
|
|
}
|
|
|
|
}
|
2017-01-20 11:19:15 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
checkForEnter(e) {
|
|
|
|
if (e.keyCode === 38 || e.keyCode === 40) {
|
|
|
|
const selectionStart = this.filteredSearchInput.selectionStart;
|
2017-01-20 11:19:15 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
e.preventDefault();
|
|
|
|
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
|
|
|
|
}
|
2017-01-20 11:19:15 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (e.keyCode === 13) {
|
|
|
|
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
|
|
|
const dropdownEl = dropdown.element;
|
|
|
|
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
|
2016-12-09 21:20:41 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
e.preventDefault();
|
2017-03-07 15:54:45 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (!activeElements.length) {
|
|
|
|
if (this.isHandledAsync) {
|
|
|
|
e.stopImmediatePropagation();
|
2016-12-09 21:20:41 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.filteredSearchInput.blur();
|
|
|
|
this.dropdownManager.resetDropdowns();
|
|
|
|
} else {
|
|
|
|
// Prevent droplab from opening dropdown
|
|
|
|
this.dropdownManager.destroyDroplab();
|
2017-01-20 11:19:15 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
|
|
|
|
this.search();
|
2016-11-04 21:27:11 +00:00
|
|
|
}
|
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2016-11-04 21:27:11 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
addInputContainerFocus() {
|
2017-06-27 18:37:30 +00:00
|
|
|
addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-03-10 22:30:42 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
removeInputContainerFocus(e) {
|
|
|
|
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
|
|
|
|
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
|
|
|
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
|
|
|
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
|
2017-03-10 22:30:42 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
|
|
|
|
!isElementInStaticFilterDropdown && inputContainer) {
|
|
|
|
inputContainer.classList.remove('focus');
|
2017-03-10 22:30:42 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-03-10 22:30:42 +00:00
|
|
|
|
2017-04-26 20:12:33 +00:00
|
|
|
removeToken(e) {
|
|
|
|
const removeButtonSelected = e.target.closest('.remove-token');
|
|
|
|
|
|
|
|
if (removeButtonSelected) {
|
|
|
|
e.preventDefault();
|
2017-06-06 15:48:45 +00:00
|
|
|
// Prevent editToken from being triggered after token is removed
|
|
|
|
e.stopImmediatePropagation();
|
2017-04-26 20:12:33 +00:00
|
|
|
|
|
|
|
const button = e.target.closest('.selectable');
|
|
|
|
gl.FilteredSearchVisualTokens.selectToken(button, true);
|
|
|
|
this.removeSelectedToken();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
unselectEditTokens(e) {
|
|
|
|
const inputContainer = this.container.querySelector('.filtered-search-box');
|
|
|
|
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
|
|
|
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
|
|
|
|
const isElementTokensContainer = e.target.classList.contains('tokens-container');
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
|
|
|
|
gl.FilteredSearchVisualTokens.moveInputToTheRight();
|
|
|
|
this.dropdownManager.resetDropdowns();
|
2017-01-30 22:53:18 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
editToken(e) {
|
|
|
|
const token = e.target.closest('.js-visual-token');
|
2017-06-06 15:48:45 +00:00
|
|
|
const sanitizedTokenName = token && token.querySelector('.name').textContent.trim();
|
2017-05-22 19:58:23 +00:00
|
|
|
const canEdit = this.canEdit && this.canEdit(sanitizedTokenName);
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-05-22 19:58:23 +00:00
|
|
|
if (token && canEdit) {
|
2017-06-06 15:48:45 +00:00
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
2017-04-17 07:01:11 +00:00
|
|
|
gl.FilteredSearchVisualTokens.editToken(token);
|
|
|
|
this.tokenChange();
|
2017-01-30 22:53:18 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
toggleClearSearchButton() {
|
|
|
|
const query = gl.DropdownUtils.getSearchQuery();
|
|
|
|
const hidden = 'hidden';
|
|
|
|
const hasHidden = this.clearSearchButton.classList.contains(hidden);
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (query.length === 0 && !hasHidden) {
|
|
|
|
this.clearSearchButton.classList.add(hidden);
|
|
|
|
} else if (query.length && hasHidden) {
|
|
|
|
this.clearSearchButton.classList.remove(hidden);
|
2017-01-30 22:53:18 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
handleInputPlaceholder() {
|
|
|
|
const query = gl.DropdownUtils.getSearchQuery();
|
|
|
|
const placeholder = 'Search or filter results...';
|
|
|
|
const currentPlaceholder = this.filteredSearchInput.placeholder;
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (query.length === 0 && currentPlaceholder !== placeholder) {
|
|
|
|
this.filteredSearchInput.placeholder = placeholder;
|
|
|
|
} else if (query.length > 0 && currentPlaceholder !== '') {
|
|
|
|
this.filteredSearchInput.placeholder = '';
|
2017-01-30 22:53:18 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-26 20:12:33 +00:00
|
|
|
removeSelectedTokenKeydown(e) {
|
2017-04-17 07:01:11 +00:00
|
|
|
// 8 = Backspace Key
|
|
|
|
// 46 = Delete Key
|
|
|
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
2017-04-26 20:12:33 +00:00
|
|
|
this.removeSelectedToken();
|
2016-12-12 22:37:49 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-26 20:12:33 +00:00
|
|
|
removeSelectedToken() {
|
|
|
|
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
|
|
|
this.handleInputPlaceholder();
|
|
|
|
this.toggleClearSearchButton();
|
|
|
|
this.dropdownManager.updateCurrentDropdownOffset();
|
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
onClearSearch(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
this.clearSearch();
|
|
|
|
}
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
clearSearch() {
|
|
|
|
this.filteredSearchInput.value = '';
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
const removeElements = [];
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
[].forEach.call(this.tokensContainer.children, (t) => {
|
2017-09-06 18:44:59 +00:00
|
|
|
let canClearToken = t.classList.contains('js-visual-token');
|
|
|
|
|
|
|
|
if (canClearToken) {
|
|
|
|
const tokenKey = t.querySelector('.name').textContent.trim();
|
|
|
|
canClearToken = this.canEdit && this.canEdit(tokenKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (canClearToken) {
|
2017-04-17 07:01:11 +00:00
|
|
|
removeElements.push(t);
|
|
|
|
}
|
|
|
|
});
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
removeElements.forEach((el) => {
|
|
|
|
el.parentElement.removeChild(el);
|
|
|
|
});
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.clearSearchButton.classList.add('hidden');
|
|
|
|
this.handleInputPlaceholder();
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.dropdownManager.resetDropdowns();
|
2017-03-07 15:54:45 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (this.isHandledAsync) {
|
|
|
|
this.search();
|
2016-12-12 22:37:49 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
handleInputVisualToken() {
|
|
|
|
const input = this.filteredSearchInput;
|
|
|
|
const { tokens, searchToken }
|
2017-05-23 16:35:54 +00:00
|
|
|
= this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys());
|
2017-04-17 07:01:11 +00:00
|
|
|
const { isLastVisualTokenValid }
|
|
|
|
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
|
|
|
|
if (isLastVisualTokenValid) {
|
|
|
|
tokens.forEach((t) => {
|
|
|
|
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
|
|
|
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
|
|
|
|
});
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
const fragments = searchToken.split(':');
|
|
|
|
if (fragments.length > 1) {
|
|
|
|
const inputValues = fragments[0].split(' ');
|
2017-08-04 08:07:05 +00:00
|
|
|
const tokenKey = _.last(inputValues);
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (inputValues.length > 1) {
|
|
|
|
inputValues.pop();
|
|
|
|
const searchTerms = inputValues.join(' ');
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
input.value = input.value.replace(searchTerms, '');
|
|
|
|
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
|
2017-01-30 22:53:18 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
|
|
|
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
|
|
|
|
input.value = input.value.replace(`${tokenKey}:`, '');
|
2017-01-30 22:53:18 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
} else {
|
|
|
|
// Keep listening to token until we determine that the user is done typing the token value
|
|
|
|
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
|
2017-01-30 22:53:18 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
|
|
|
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
|
2017-01-21 07:44:36 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
// Trim the last space as seen in the if statement above
|
|
|
|
input.value = input.value.replace(searchToken, '').trim();
|
|
|
|
}
|
2017-03-30 02:04:05 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-03-30 02:04:05 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
handleFormSubmit(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
this.search();
|
|
|
|
}
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
saveCurrentSearchQuery() {
|
|
|
|
// Don't save before we have fetched the already saved searches
|
|
|
|
this.fetchingRecentSearchesPromise.then(() => {
|
|
|
|
const searchQuery = gl.DropdownUtils.getSearchQuery();
|
|
|
|
if (searchQuery.length > 0) {
|
|
|
|
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
|
|
|
|
this.recentSearchesService.save(resultantSearches);
|
|
|
|
}
|
2017-04-20 11:11:51 +00:00
|
|
|
}).catch(() => {
|
|
|
|
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
|
2017-04-17 07:01:11 +00:00
|
|
|
});
|
|
|
|
}
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-09-06 18:44:59 +00:00
|
|
|
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
|
|
|
|
getAllParams(urlParams) {
|
|
|
|
return this.modifyUrlParams ? this.modifyUrlParams(urlParams) : urlParams;
|
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
loadSearchParamsFromURL() {
|
2017-09-06 18:44:59 +00:00
|
|
|
const urlParams = gl.utils.getUrlParamsArray();
|
|
|
|
const params = this.getAllParams(urlParams);
|
2017-04-17 07:01:11 +00:00
|
|
|
const usernameParams = this.getUsernameParams();
|
|
|
|
let hasFilteredSearch = false;
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
params.forEach((p) => {
|
|
|
|
const split = p.split('=');
|
|
|
|
const keyParam = decodeURIComponent(split[0]);
|
|
|
|
const value = split[1];
|
|
|
|
|
|
|
|
// Check if it matches edge conditions listed in this.filteredSearchTokenKeys
|
|
|
|
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (condition) {
|
|
|
|
hasFilteredSearch = true;
|
2017-05-22 19:58:23 +00:00
|
|
|
const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
|
|
|
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(
|
|
|
|
condition.tokenKey,
|
|
|
|
condition.value,
|
|
|
|
canEdit,
|
|
|
|
);
|
2017-04-17 07:01:11 +00:00
|
|
|
} else {
|
|
|
|
// Sanitize value since URL converts spaces into +
|
|
|
|
// Replace before decode so that we know what was originally + versus the encoded +
|
|
|
|
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
|
|
|
|
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
|
|
|
|
|
|
|
|
if (match) {
|
2017-08-30 07:48:55 +00:00
|
|
|
// Use lastIndexOf because the token key is allowed to contain underscore
|
|
|
|
// e.g. 'my_reaction' is the token key of 'my_reaction_emoji'
|
|
|
|
const lastIndexOf = keyParam.lastIndexOf('_');
|
|
|
|
let sanitizedKey = lastIndexOf !== -1 ? keyParam.slice(0, lastIndexOf) : keyParam;
|
|
|
|
// Replace underscore with hyphen in the sanitizedkey.
|
|
|
|
// e.g. 'my_reaction' => 'my-reaction'
|
|
|
|
sanitizedKey = sanitizedKey.replace('_', '-');
|
2017-04-17 07:01:11 +00:00
|
|
|
const symbol = match.symbol;
|
|
|
|
let quotationsToUse = '';
|
|
|
|
|
|
|
|
if (sanitizedValue.indexOf(' ') !== -1) {
|
|
|
|
// Prefer ", but use ' if required
|
|
|
|
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
|
|
|
|
}
|
|
|
|
|
|
|
|
hasFilteredSearch = true;
|
2017-05-22 19:58:23 +00:00
|
|
|
const canEdit = this.canEdit && this.canEdit(sanitizedKey);
|
|
|
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(
|
|
|
|
sanitizedKey,
|
|
|
|
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
|
|
|
|
canEdit,
|
|
|
|
);
|
2017-04-17 07:01:11 +00:00
|
|
|
} else if (!match && keyParam === 'assignee_id') {
|
|
|
|
const id = parseInt(value, 10);
|
|
|
|
if (usernameParams[id]) {
|
2017-01-30 22:53:18 +00:00
|
|
|
hasFilteredSearch = true;
|
2017-05-22 19:58:23 +00:00
|
|
|
const tokenName = 'assignee';
|
|
|
|
const canEdit = this.canEdit && this.canEdit(tokenName);
|
|
|
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
|
|
|
} else if (!match && keyParam === 'author_id') {
|
|
|
|
const id = parseInt(value, 10);
|
|
|
|
if (usernameParams[id]) {
|
2017-01-30 22:53:18 +00:00
|
|
|
hasFilteredSearch = true;
|
2017-05-22 19:58:23 +00:00
|
|
|
const tokenName = 'author';
|
|
|
|
const canEdit = this.canEdit && this.canEdit(tokenName);
|
|
|
|
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
|
2016-12-12 22:37:49 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
} else if (!match && keyParam === 'search') {
|
|
|
|
hasFilteredSearch = true;
|
|
|
|
this.filteredSearchInput.value = sanitizedValue;
|
2016-12-12 22:37:49 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
|
|
|
});
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.saveCurrentSearchQuery();
|
2017-03-30 02:04:05 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (hasFilteredSearch) {
|
|
|
|
this.clearSearchButton.classList.remove('hidden');
|
|
|
|
this.handleInputPlaceholder();
|
2016-12-12 22:37:49 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2016-12-12 22:37:49 +00:00
|
|
|
|
2017-06-06 14:58:59 +00:00
|
|
|
searchState(e) {
|
2017-06-19 18:59:17 +00:00
|
|
|
e.preventDefault();
|
2017-06-06 14:58:59 +00:00
|
|
|
const target = e.currentTarget;
|
2017-06-06 16:06:00 +00:00
|
|
|
// remove focus outline after click
|
|
|
|
target.blur();
|
|
|
|
|
2017-06-07 15:54:54 +00:00
|
|
|
const state = target.dataset && target.dataset.state;
|
2017-06-06 14:58:59 +00:00
|
|
|
|
2017-06-07 15:54:54 +00:00
|
|
|
if (state) {
|
2017-06-06 23:18:31 +00:00
|
|
|
this.search(state);
|
|
|
|
}
|
2017-06-06 14:58:59 +00:00
|
|
|
}
|
|
|
|
|
2017-06-06 01:06:38 +00:00
|
|
|
search(state = null) {
|
2017-04-17 07:01:11 +00:00
|
|
|
const paths = [];
|
|
|
|
const searchQuery = gl.DropdownUtils.getSearchQuery();
|
2017-03-30 02:04:05 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.saveCurrentSearchQuery();
|
2016-12-13 03:15:50 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
const { tokens, searchToken }
|
2017-05-23 16:35:54 +00:00
|
|
|
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
|
2017-06-06 01:06:38 +00:00
|
|
|
const currentState = state || gl.utils.getParameterByName('state') || 'opened';
|
2017-04-17 07:01:11 +00:00
|
|
|
paths.push(`state=${currentState}`);
|
2016-11-14 19:22:32 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
tokens.forEach((token) => {
|
|
|
|
const condition = this.filteredSearchTokenKeys
|
|
|
|
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
|
|
|
|
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
|
2017-08-30 07:48:55 +00:00
|
|
|
// Replace hyphen with underscore to use as request parameter
|
|
|
|
// e.g. 'my-reaction' => 'my_reaction'
|
|
|
|
const underscoredKey = token.key.replace('-', '_');
|
|
|
|
const keyParam = param ? `${underscoredKey}_${param}` : underscoredKey;
|
2017-04-17 07:01:11 +00:00
|
|
|
let tokenPath = '';
|
2017-01-06 19:56:34 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (condition) {
|
|
|
|
tokenPath = condition.url;
|
|
|
|
} else {
|
|
|
|
let tokenValue = token.value;
|
2017-01-06 19:56:34 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
|
|
|
|
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
|
|
|
|
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
|
2016-11-14 19:22:32 +00:00
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
|
2016-11-04 21:27:11 +00:00
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
paths.push(tokenPath);
|
|
|
|
});
|
2017-01-13 21:54:16 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (searchToken) {
|
|
|
|
const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
|
|
|
|
paths.push(`search=${sanitized}`);
|
2016-11-04 21:27:11 +00:00
|
|
|
}
|
2017-01-20 21:35:39 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
|
|
|
|
|
|
|
|
if (this.updateObject) {
|
|
|
|
this.updateObject(parameterizedUrl);
|
|
|
|
} else {
|
|
|
|
gl.utils.visitUrl(parameterizedUrl);
|
2017-01-20 21:35:39 +00:00
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
2017-01-18 21:39:43 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
getUsernameParams() {
|
|
|
|
const usernamesById = {};
|
|
|
|
try {
|
|
|
|
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
|
|
|
|
JSON.parse(attribute).forEach((user) => {
|
|
|
|
usernamesById[user.id] = user.username;
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
return usernamesById;
|
|
|
|
}
|
2017-01-18 21:39:43 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
tokenChange() {
|
|
|
|
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
|
2017-03-08 02:12:23 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
if (dropdown) {
|
|
|
|
const currentDropdownRef = dropdown.reference;
|
2017-03-30 02:04:05 +00:00
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
this.setDropdownWrapper();
|
|
|
|
currentDropdownRef.dispatchInputEvent();
|
2017-03-30 02:04:05 +00:00
|
|
|
}
|
2016-11-04 21:27:11 +00:00
|
|
|
}
|
|
|
|
|
2017-04-17 07:01:11 +00:00
|
|
|
onrecentSearchesItemSelected(text) {
|
|
|
|
this.clearSearch();
|
|
|
|
this.filteredSearchInput.value = text;
|
|
|
|
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
|
|
|
|
this.search();
|
|
|
|
}
|
2017-05-22 19:58:23 +00:00
|
|
|
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
|
|
canEdit() {
|
|
|
|
return true;
|
|
|
|
}
|
2017-04-17 07:01:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
window.gl = window.gl || {};
|
|
|
|
gl.FilteredSearchManager = FilteredSearchManager;
|