236 lines
7.4 KiB
JavaScript
236 lines
7.4 KiB
JavaScript
import { last } from 'lodash';
|
|
import AvailableDropdownMappings from 'ee_else_ce/filtered_search/available_dropdown_mappings';
|
|
import DropLab from '~/droplab/drop_lab';
|
|
import { DROPDOWN_TYPE } from './constants';
|
|
import FilteredSearchContainer from './container';
|
|
import DropdownUtils from './dropdown_utils';
|
|
import FilteredSearchTokenKeys from './filtered_search_token_keys';
|
|
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
|
|
|
|
export default class FilteredSearchDropdownManager {
|
|
constructor({
|
|
runnerTagsEndpoint = '',
|
|
labelsEndpoint = '',
|
|
milestonesEndpoint = '',
|
|
iterationsEndpoint = '',
|
|
releasesEndpoint = '',
|
|
environmentsEndpoint = '',
|
|
epicsEndpoint = '',
|
|
tokenizer,
|
|
page,
|
|
isGroup,
|
|
isGroupAncestor,
|
|
isGroupDecendent,
|
|
filteredSearchTokenKeys,
|
|
}) {
|
|
const removeTrailingSlash = (url) => url.replace(/\/$/, '');
|
|
|
|
this.container = FilteredSearchContainer.container;
|
|
this.runnerTagsEndpoint = removeTrailingSlash(runnerTagsEndpoint);
|
|
this.labelsEndpoint = removeTrailingSlash(labelsEndpoint);
|
|
this.milestonesEndpoint = removeTrailingSlash(milestonesEndpoint);
|
|
this.iterationsEndpoint = removeTrailingSlash(iterationsEndpoint);
|
|
this.releasesEndpoint = removeTrailingSlash(releasesEndpoint);
|
|
this.epicsEndpoint = removeTrailingSlash(epicsEndpoint);
|
|
this.environmentsEndpoint = removeTrailingSlash(environmentsEndpoint);
|
|
this.tokenizer = tokenizer;
|
|
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
|
|
this.filteredSearchInput = this.container.querySelector('.filtered-search');
|
|
this.page = page;
|
|
this.groupsOnly = isGroup;
|
|
this.includeAncestorGroups = isGroupAncestor;
|
|
this.includeDescendantGroups = isGroupDecendent;
|
|
|
|
this.setupMapping();
|
|
|
|
this.cleanupWrapper = this.cleanup.bind(this);
|
|
document.addEventListener('beforeunload', this.cleanupWrapper);
|
|
}
|
|
|
|
cleanup() {
|
|
if (this.droplab) {
|
|
this.droplab.destroy();
|
|
this.droplab = null;
|
|
}
|
|
|
|
this.setupMapping();
|
|
|
|
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
|
}
|
|
|
|
setupMapping() {
|
|
const supportedTokens = this.filteredSearchTokenKeys.getKeys();
|
|
|
|
const availableMappings = new AvailableDropdownMappings({ ...this });
|
|
|
|
this.mapping = availableMappings.getAllowedMappings(supportedTokens);
|
|
}
|
|
|
|
static addWordToInput({
|
|
tokenName,
|
|
tokenOperator = '',
|
|
tokenValue = '',
|
|
clicked = false,
|
|
options = {},
|
|
}) {
|
|
const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
|
|
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenOperator, tokenValue, {
|
|
uppercaseTokenName,
|
|
capitalizeTokenValue,
|
|
});
|
|
input.value = '';
|
|
|
|
if (clicked) {
|
|
FilteredSearchVisualTokens.moveInputToTheRight();
|
|
}
|
|
}
|
|
|
|
updateCurrentDropdownOffset() {
|
|
this.updateDropdownOffset(this.currentDropdown);
|
|
}
|
|
|
|
updateDropdownOffset(key) {
|
|
// Always align dropdown with the input field
|
|
let offset =
|
|
this.filteredSearchInput.getBoundingClientRect().left -
|
|
this.container.querySelector('.scroll-container').getBoundingClientRect().left;
|
|
|
|
const maxInputWidth = 240;
|
|
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
|
|
|
|
// Make sure offset never exceeds the input container
|
|
const offsetMaxWidth =
|
|
this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
|
|
if (offsetMaxWidth < offset) {
|
|
offset = offsetMaxWidth;
|
|
}
|
|
|
|
this.mapping[key].reference.setOffset(offset);
|
|
}
|
|
|
|
load(key, firstLoad = false, dropdownKey = '') {
|
|
const mappingKey = this.mapping[key];
|
|
const glClass = mappingKey.gl;
|
|
const { element } = mappingKey;
|
|
let forceShowList = false;
|
|
|
|
if (!mappingKey.reference) {
|
|
const defaultArguments = {
|
|
droplab: this.droplab,
|
|
dropdown: element,
|
|
input: this.filteredSearchInput,
|
|
tokenKeys: this.filteredSearchTokenKeys,
|
|
filter: key,
|
|
};
|
|
const extraArguments = mappingKey.extraArguments || {};
|
|
const glArguments = { ...defaultArguments, ...extraArguments };
|
|
|
|
// Passing glArguments to `new glClass(<arguments>)`
|
|
mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
|
|
}
|
|
|
|
if (firstLoad) {
|
|
mappingKey.reference.init();
|
|
}
|
|
|
|
if (
|
|
this.currentDropdown === DROPDOWN_TYPE.hint ||
|
|
this.currentDropdown === DROPDOWN_TYPE.operator
|
|
) {
|
|
// Force the dropdown to show if it was clicked from the hint dropdown
|
|
forceShowList = true;
|
|
}
|
|
|
|
this.updateDropdownOffset(key);
|
|
mappingKey.reference.render(firstLoad, forceShowList, dropdownKey);
|
|
|
|
this.currentDropdown = key;
|
|
}
|
|
|
|
loadDropdown(dropdownName = '', dropdownKey = '') {
|
|
let firstLoad = false;
|
|
|
|
if (!this.droplab) {
|
|
firstLoad = true;
|
|
this.droplab = new DropLab();
|
|
}
|
|
|
|
if (dropdownName === DROPDOWN_TYPE.operator) {
|
|
this.load(dropdownName, firstLoad, dropdownKey);
|
|
return;
|
|
}
|
|
|
|
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
|
|
const shouldOpenFilterDropdown =
|
|
match && this.currentDropdown !== match.key && this.mapping[match.key];
|
|
const shouldOpenHintDropdown = !match && this.currentDropdown !== DROPDOWN_TYPE.hint;
|
|
|
|
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
|
|
const key = match && match.key ? match.key : DROPDOWN_TYPE.hint;
|
|
|
|
this.load(key, firstLoad, dropdownKey);
|
|
}
|
|
}
|
|
|
|
setDropdown() {
|
|
const query = DropdownUtils.getSearchQuery(true);
|
|
const { lastToken, searchToken } = this.tokenizer.processTokens(
|
|
query,
|
|
this.filteredSearchTokenKeys.getKeys(),
|
|
);
|
|
|
|
if (this.currentDropdown) {
|
|
this.updateCurrentDropdownOffset();
|
|
}
|
|
if (lastToken === searchToken && lastToken !== null) {
|
|
// Token is not fully initialized yet because it has no value
|
|
// Eg. token = 'label:'
|
|
|
|
const split = lastToken.split(':');
|
|
const dropdownName = last(split[0].split(' '));
|
|
const possibleOperatorToken = last(split[1]);
|
|
|
|
const hasOperator = FilteredSearchVisualTokens.permissibleOperatorValues.includes(
|
|
possibleOperatorToken && possibleOperatorToken.trim(),
|
|
);
|
|
|
|
let dropdownToOpen = '';
|
|
|
|
if (split.length > 1) {
|
|
const lastOperatorToken = FilteredSearchVisualTokens.getLastTokenOperator();
|
|
dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator;
|
|
}
|
|
|
|
this.loadDropdown(dropdownToOpen, dropdownName);
|
|
} else if (lastToken) {
|
|
const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator();
|
|
// Token has been initialized into an object because it has a value
|
|
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator, lastToken.key);
|
|
} else {
|
|
this.loadDropdown(DROPDOWN_TYPE.hint);
|
|
}
|
|
}
|
|
|
|
resetDropdowns() {
|
|
if (!this.currentDropdown) {
|
|
return;
|
|
}
|
|
|
|
// Force current dropdown to hide
|
|
this.mapping[this.currentDropdown].reference.hideDropdown();
|
|
|
|
// Re-Load dropdown
|
|
this.setDropdown();
|
|
|
|
// Reset filters for current dropdown
|
|
this.mapping[this.currentDropdown].reference.resetFilters();
|
|
|
|
// Reposition dropdown so that it is aligned with cursor
|
|
this.updateDropdownOffset(this.currentDropdown);
|
|
}
|
|
|
|
destroyDroplab() {
|
|
this.droplab.destroy();
|
|
}
|
|
}
|