Commit 5613c325 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'remove-iife-filtered-search-bundle' into 'master'

Remove IIFE's in filtered_search_bundle.js

See merge request !10730
parents bbd83376 08a09c6b
...@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter'; ...@@ -2,82 +2,80 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownHint extends gl.FilteredSearchDropdown {
class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.config = {
this.config = { Filter: {
Filter: { template: 'hint',
template: 'hint', filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
filterFunction: gl.DropdownUtils.filterHint.bind(null, input), },
}, };
}; }
}
itemClicked(e) {
const { selected } = e.detail;
if (selected.tagName === 'LI') { itemClicked(e) {
if (selected.hasAttribute('data-value')) { const { selected } = e.detail;
this.dismissDropdown();
} else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) { if (selected.tagName === 'LI') {
// Get previous input values in the input field and convert them into visual tokens if (selected.hasAttribute('data-value')) {
const previousInputValues = this.input.value.split(' '); this.dismissDropdown();
const searchTerms = []; } else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
previousInputValues.forEach((value, index) => { if (tag.length) {
searchTerms.push(value); // Get previous input values in the input field and convert them into visual tokens
const previousInputValues = this.input.value.split(' ');
const searchTerms = [];
if (index === previousInputValues.length - 1 previousInputValues.forEach((value, index) => {
&& token.indexOf(value.toLowerCase()) !== -1) { searchTerms.push(value);
searchTerms.pop();
}
});
if (searchTerms.length > 0) { if (index === previousInputValues.length - 1
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); && token.indexOf(value.toLowerCase()) !== -1) {
searchTerms.pop();
} }
});
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); if (searchTerms.length > 0) {
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
} }
this.dismissDropdown();
this.dispatchInputEvent(); gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
} }
this.dismissDropdown();
this.dispatchInputEvent();
} }
} }
}
renderContent() { renderContent() {
const dropdownData = []; const dropdownData = [];
[].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag, type } = dropdownMenu.dataset; const { icon, hint, tag, type } = dropdownMenu.dataset;
if (icon && hint && tag) { if (icon && hint && tag) {
dropdownData.push( dropdownData.push(
Object.assign({ Object.assign({
icon: `fa-${icon}`, icon: `fa-${icon}`,
hint, hint,
tag: `<${tag}>`, tag: `<${tag}>`,
}, type && { type }), }, type && { type }),
); );
} }
}); });
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);
} }
init() { init() {
this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init(); this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownHint = DropdownHint; gl.DropdownHint = DropdownHint;
})();
...@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter'; ...@@ -5,48 +5,46 @@ import Filter from '~/droplab/plugins/filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownNonUser extends gl.FilteredSearchDropdown {
class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter, endpoint, symbol) {
constructor(droplab, dropdown, input, filter, endpoint, symbol) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.symbol = symbol;
this.symbol = symbol; this.config = {
this.config = { Ajax: {
Ajax: { endpoint,
endpoint, method: 'setData',
method: 'setData', loadingTemplate: this.loadingTemplate,
loadingTemplate: this.loadingTemplate, onError() {
onError() { /* eslint-disable no-new */
/* eslint-disable no-new */ new Flash('An error occured fetching the dropdown data.');
new Flash('An error occured fetching the dropdown data.'); /* eslint-enable no-new */
/* eslint-enable no-new */
},
}, },
Filter: { },
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), Filter: {
template: 'title', filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
}, template: 'title',
}; },
} };
}
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, (selected) => { super.itemClicked(e, (selected) => {
const title = selected.querySelector('.js-data-value').innerText.trim(); const title = selected.querySelector('.js-data-value').innerText.trim();
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
}); });
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
this.droplab this.droplab
.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config); .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
super.renderContent(forceShowList); super.renderContent(forceShowList);
} }
init() { init() {
this.droplab this.droplab
.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownNonUser = DropdownNonUser; gl.DropdownNonUser = DropdownNonUser;
})();
...@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter'; ...@@ -4,69 +4,67 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter';
require('./filtered_search_dropdown'); require('./filtered_search_dropdown');
(() => { class DropdownUser extends gl.FilteredSearchDropdown {
class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { super(droplab, dropdown, input, filter);
super(droplab, dropdown, input, filter); this.config = {
this.config = { AjaxFilter: {
AjaxFilter: { endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, searchKey: 'search',
searchKey: 'search', params: {
params: { per_page: 20,
per_page: 20, active: true,
active: true, project_id: this.getProjectId(),
project_id: this.getProjectId(), current_user: true,
current_user: true,
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
/* eslint-enable no-new */
},
}, },
}; searchValueFunction: this.getSearchInput.bind(this),
} loadingTemplate: this.loadingTemplate,
onError() {
itemClicked(e) { /* eslint-disable no-new */
super.itemClicked(e, new Flash('An error occured fetching the dropdown data.');
selected => selected.querySelector('.dropdown-light-content').innerText.trim()); /* eslint-enable no-new */
} },
},
renderContent(forceShowList = false) { };
this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config); }
super.renderContent(forceShowList);
}
getProjectId() { itemClicked(e) {
return this.input.getAttribute('data-project-id'); super.itemClicked(e,
} selected => selected.querySelector('.dropdown-light-content').innerText.trim());
}
getSearchInput() { renderContent(forceShowList = false) {
const query = gl.DropdownUtils.getSearchInput(this.input); this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); super.renderContent(forceShowList);
}
let value = lastToken || ''; getProjectId() {
return this.input.getAttribute('data-project-id');
}
if (value[0] === '@') { getSearchInput() {
value = value.slice(1); const query = gl.DropdownUtils.getSearchInput(this.input);
} const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
// Removes the first character if it is a quotation so that we can search let value = lastToken || '';
// with multiple words
if (value[0] === '"' || value[0] === '\'') {
value = value.slice(1);
}
return value; if (value[0] === '@') {
value = value.slice(1);
} }
init() { // Removes the first character if it is a quotation so that we can search
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init(); // with multiple words
if (value[0] === '"' || value[0] === '\'') {
value = value.slice(1);
} }
return value;
}
init() {
this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownUser = DropdownUser; gl.DropdownUser = DropdownUser;
})();
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
(() => { class DropdownUtils {
class DropdownUtils { static getEscapedText(text) {
static getEscapedText(text) { let escapedText = text;
let escapedText = text; const hasSpace = text.indexOf(' ') !== -1;
const hasSpace = text.indexOf(' ') !== -1; const hasDoubleQuote = text.indexOf('"') !== -1;
const hasDoubleQuote = text.indexOf('"') !== -1;
// Encapsulate value with quotes if it has spaces
// Encapsulate value with quotes if it has spaces // Known side effect: values's with both single and double quotes
// Known side effect: values's with both single and double quotes // won't escape properly
// won't escape properly if (hasSpace) {
if (hasSpace) { if (hasDoubleQuote) {
if (hasDoubleQuote) { escapedText = `'${text}'`;
escapedText = `'${text}'`; } else {
} else { // Encapsulate singleQuotes or if it hasSpace
// Encapsulate singleQuotes or if it hasSpace escapedText = `"${text}"`;
escapedText = `"${text}"`;
}
} }
return escapedText;
} }
static filterWithSymbol(filterSymbol, input, item) { return escapedText;
const updatedItem = item; }
const searchInput = gl.DropdownUtils.getSearchInput(input);
static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
const searchInput = gl.DropdownUtils.getSearchInput(input);
const title = updatedItem.title.toLowerCase(); const title = updatedItem.title.toLowerCase();
let value = searchInput.toLowerCase(); let value = searchInput.toLowerCase();
let symbol = ''; let symbol = '';
// Remove the symbol for filter // Remove the symbol for filter
if (value[0] === filterSymbol) { if (value[0] === filterSymbol) {
symbol = value[0]; symbol = value[0];
value = value.slice(1); value = value.slice(1);
} }
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1); value = value.slice(1);
} }
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${symbol}${value}`) !== -1;
// Eg. filterSymbol = ~ for labels updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol; return updatedItem;
}
return updatedItem; static filterHint(input, item) {
const updatedItem = item;
const searchInput = gl.DropdownUtils.getSearchQuery(input);
const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
const lastKey = lastToken.key || lastToken || '';
const allowMultiple = item.type === 'array';
const itemInExistingTokens = tokens.some(t => t.key === item.hint);
if (!allowMultiple && itemInExistingTokens) {
updatedItem.droplab_hidden = true;
} else if (!lastKey || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastKey) {
const split = lastKey.split(':');
const tokenName = split[0].split(' ').last();
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
updatedItem.droplab_hidden = tokenName ? match : false;
} }
static filterHint(input, item) { return updatedItem;
const updatedItem = item; }
const searchInput = gl.DropdownUtils.getSearchQuery(input);
const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput); static setDataValueIfSelected(filter, selected) {
const lastKey = lastToken.key || lastToken || ''; const dataValue = selected.getAttribute('data-value');
const allowMultiple = item.type === 'array';
const itemInExistingTokens = tokens.some(t => t.key === item.hint);
if (!allowMultiple && itemInExistingTokens) {
updatedItem.droplab_hidden = true;
} else if (!lastKey || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastKey) {
const split = lastKey.split(':');
const tokenName = split[0].split(' ').last();
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
updatedItem.droplab_hidden = tokenName ? match : false;
}
return updatedItem; if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
} }
static setDataValueIfSelected(filter, selected) { // Return boolean based on whether it was set
const dataValue = selected.getAttribute('data-value'); return dataValue !== null;
}
if (dataValue) { // Determines the full search query (visual tokens + input)
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); static getSearchQuery(untilInput = false) {
} const container = FilteredSearchContainer.container;
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
const values = [];
// Return boolean based on whether it was set if (untilInput) {
return dataValue !== null; const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
// Add one to include input-token to the tokens array
tokens.splice(inputIndex + 1);
} }
// Determines the full search query (visual tokens + input) tokens.forEach((token) => {
static getSearchQuery(untilInput = false) { if (token.classList.contains('js-visual-token')) {
const container = FilteredSearchContainer.container; const name = token.querySelector('.name');
const tokens = [].slice.call(container.querySelectorAll('.tokens-container li')); const value = token.querySelector('.value');
const values = []; const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
if (untilInput) { if (value && value.innerText) {
const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token')); valueText = value.innerText;
// Add one to include input-token to the tokens array }
tokens.splice(inputIndex + 1);
}
tokens.forEach((token) => { if (token.className.indexOf('filtered-search-token') !== -1) {
if (token.classList.contains('js-visual-token')) { values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
const name = token.querySelector('.name'); } else {
const value = token.querySelector('.value'); values.push(name.innerText);
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
if (value && value.innerText) {
valueText = value.innerText;
}
if (token.className.indexOf('filtered-search-token') !== -1) {
values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
} else {
values.push(name.innerText);
}
} else if (token.classList.contains('input-token')) {
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const inputValue = input && input.value;
if (isLastVisualTokenValid) {
values.push(inputValue);
} else {
const previous = values.pop();
values.push(`${previous}${inputValue}`);
}
} }
}); } else if (token.classList.contains('input-token')) {
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
return values const input = FilteredSearchContainer.container.querySelector('.filtered-search');
.map(value => value.trim()) const inputValue = input && input.value;
.join(' ');
}
static getSearchInput(filteredSearchInput) { if (isLastVisualTokenValid) {
const inputValue = filteredSearchInput.value; values.push(inputValue);
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); } else {
const previous = values.pop();
values.push(`${previous}${inputValue}`);
}
}
});
return inputValue.slice(0, right); return values
} .map(value => value.trim())
.join(' ');
}
static getInputSelectionPosition(input) { static getSearchInput(filteredSearchInput) {
const selectionStart = input.selectionStart; const inputValue = filteredSearchInput.value;
let inputValue = input.value; const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
// Replace all spaces inside quote marks with underscores
// (will continue to match entire string until an end quote is found if any)
// This helps with matching the beginning & end of a token:key
inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
let right = inputValue.slice(selectionStart).search(/\s/);
if (right >= 0) {
right += selectionStart;
} else if (right < 0) {
right = inputValue.length;
}
// Get the left position for the word selected return inputValue.slice(0, right);
// Regex matches last non-whitespace character }
let left = inputValue.slice(0, right).search(/\S+$/);
if (selectionStart === 0) { static getInputSelectionPosition(input) {
left = 0; const selectionStart = input.selectionStart;
} else if (selectionStart === inputValue.length && left < 0) { let inputValue = input.value;
left = inputValue.length; // Replace all spaces inside quote marks with underscores
} else if (left < 0) { // (will continue to match entire string until an end quote is found if any)
left = selectionStart; // This helps with matching the beginning & end of a token:key
} inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
let right = inputValue.slice(selectionStart).search(/\s/);
if (right >= 0) {
right += selectionStart;
} else if (right < 0) {
right = inputValue.length;
}
// Get the left position for the word selected
// Regex matches last non-whitespace character
let left = inputValue.slice(0, right).search(/\S+$/);
return { if (selectionStart === 0) {
left, left = 0;
right, } else if (selectionStart === inputValue.length && left < 0) {
}; left = inputValue.length;
} else if (left < 0) {
left = selectionStart;
} }
return {
left,
right,
};
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.DropdownUtils = DropdownUtils; gl.DropdownUtils = DropdownUtils;
})();
(() => { const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
class FilteredSearchDropdown { constructor(droplab, dropdown, input, filter) {
constructor(droplab, dropdown, input, filter) { this.droplab = droplab;
this.droplab = droplab; this.hookId = input && input.id;
this.hookId = input && input.id; this.input = input;
this.input = input; this.filter = filter;
this.filter = filter; this.dropdown = dropdown;
this.dropdown = dropdown; this.loadingTemplate = `<div class="filter-dropdown-loading">
this.loadingTemplate = `<div class="filter-dropdown-loading"> <i class="fa fa-spinner fa-spin"></i>
<i class="fa fa-spinner fa-spin"></i> </div>`;
</div>`; this.bindEvents();
this.bindEvents(); }
}
bindEvents() {
this.itemClickedWrapper = this.itemClicked.bind(this);
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
unbindEvents() { bindEvents() {
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); this.itemClickedWrapper = this.itemClicked.bind(this);
} this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
getCurrentHook() { unbindEvents() {
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
} }
itemClicked(e, getValueFunction) { getCurrentHook() {
const { selected } = e.detail; return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
}
if (selected.tagName === 'LI' && selected.innerHTML) { itemClicked(e, getValueFunction) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); const { selected } = e.detail;
if (!dataValueSet) { if (selected.tagName === 'LI' && selected.innerHTML) {
const value = getValueFunction(selected); const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.resetFilters(); if (!dataValueSet) {
this.dismissDropdown(); const value = getValueFunction(selected);
this.dispatchInputEvent(); gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
} }
}
setAsDropdown() { this.resetFilters();
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); this.dismissDropdown();
this.dispatchInputEvent();
} }
}
setOffset(offset = 0) { setAsDropdown() {
if (window.innerWidth > 480) { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
this.dropdown.style.left = `${offset}px`; }
} else {
this.dropdown.style.left = '0px'; setOffset(offset = 0) {
} if (window.innerWidth > 480) {
this.dropdown.style.left = `${offset}px`;
} else {
this.dropdown.style.left = '0px';
} }
}
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (forceShowList && currentHook && currentHook.list.hidden) { if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show(); currentHook.list.show();
}
} }
}
render(forceRenderContent = false, forceShowList = false) { render(forceRenderContent = false, forceShowList = false) {
this.setAsDropdown(); this.setAsDropdown();
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null; const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) { if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
} else if (currentHook.list.list.id !== this.dropdown.id) { } else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList); this.renderContent(forceShowList);
}
} }
}
dismissDropdown() { dismissDropdown() {
// Focusing on the input will dismiss dropdown // Focusing on the input will dismiss dropdown
// (default droplab functionality) // (default droplab functionality)
this.input.focus(); this.input.focus();
} }
dispatchInputEvent() { dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager // Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open // so that it can determine which dropdowns to open
this.input.dispatchEvent(new CustomEvent('input', { this.input.dispatchEvent(new CustomEvent('input', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
})); }));
} }
dispatchFormSubmitEvent() { dispatchFormSubmitEvent() {
// dispatchEvent() is necessary as form.submit() does not // dispatchEvent() is necessary as form.submit() does not
// trigger event handlers // trigger event handlers
this.input.form.dispatchEvent(new Event('submit')); this.input.form.dispatchEvent(new Event('submit'));
} }
hideDropdown() { hideDropdown() {
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
if (currentHook) { if (currentHook) {
currentHook.list.hide(); currentHook.list.hide();
}
} }
}
resetFilters() { resetFilters() {
const hook = this.getCurrentHook(); const hook = this.getCurrentHook();
if (hook) { if (hook) {
const data = hook.list.data || []; const data = hook.list.data || [];
const results = data.map((o) => { const results = data.map((o) => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
return updated; return updated;
}); });
hook.list.render(results); hook.list.render(results);
}
} }
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchDropdown = FilteredSearchDropdown; gl.FilteredSearchDropdown = FilteredSearchDropdown;
})();
import DropLab from '~/droplab/drop_lab'; import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
(() => { class FilteredSearchDropdownManager {
class FilteredSearchDropdownManager { constructor(baseEndpoint = '', page) {
constructor(baseEndpoint = '', page) { this.container = FilteredSearchContainer.container;
this.container = FilteredSearchContainer.container; this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.tokenizer = gl.FilteredSearchTokenizer;
this.tokenizer = gl.FilteredSearchTokenizer; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.page = page;
this.page = page;
this.setupMapping();
this.setupMapping();
this.cleanupWrapper = this.cleanup.bind(this);
this.cleanupWrapper = this.cleanup.bind(this); document.addEventListener('beforeunload', this.cleanupWrapper);
document.addEventListener('beforeunload', this.cleanupWrapper); }
cleanup() {
if (this.droplab) {
this.droplab.destroy();
this.droplab = null;
} }
cleanup() { this.setupMapping();
if (this.droplab) {
this.droplab.destroy();
this.droplab = null;
}
this.setupMapping(); document.removeEventListener('beforeunload', this.cleanupWrapper);
}
document.removeEventListener('beforeunload', this.cleanupWrapper); setupMapping() {
} this.mapping = {
author: {
reference: null,
gl: 'DropdownUser',
element: this.container.querySelector('#js-dropdown-author'),
},
assignee: {
reference: null,
gl: 'DropdownUser',
element: this.container.querySelector('#js-dropdown-assignee'),
},
milestone: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
element: this.container.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
element: this.container.querySelector('#js-dropdown-label'),
},
hint: {
reference: null,
gl: 'DropdownHint',
element: this.container.querySelector('#js-dropdown-hint'),
},
};
}
setupMapping() { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
this.mapping = { const input = FilteredSearchContainer.container.querySelector('.filtered-search');
author: {
reference: null, gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
gl: 'DropdownUser', input.value = '';
element: this.container.querySelector('#js-dropdown-author'),
}, if (clicked) {
assignee: { gl.FilteredSearchVisualTokens.moveInputToTheRight();
reference: null,
gl: 'DropdownUser',
element: this.container.querySelector('#js-dropdown-assignee'),
},
milestone: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
element: this.container.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
element: this.container.querySelector('#js-dropdown-label'),
},
hint: {
reference: null,
gl: 'DropdownHint',
element: this.container.querySelector('#js-dropdown-hint'),
},
};
} }
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) { updateCurrentDropdownOffset() {
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); this.updateDropdownOffset(this.currentDropdown);
}
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); updateDropdownOffset(key) {
input.value = ''; // Always align dropdown with the input field
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
if (clicked) { const maxInputWidth = 240;
gl.FilteredSearchVisualTokens.moveInputToTheRight(); const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
}
}
updateCurrentDropdownOffset() { // Make sure offset never exceeds the input container
this.updateDropdownOffset(this.currentDropdown); const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
} }
updateDropdownOffset(key) { this.mapping[key].reference.setOffset(offset);
// Always align dropdown with the input field }
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240; load(key, firstLoad = false) {
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
const element = mappingKey.element;
let forceShowList = false;
// Make sure offset never exceeds the input container if (!mappingKey.reference) {
const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; const dl = this.droplab;
if (offsetMaxWidth < offset) { const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
offset = offsetMaxWidth; const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
}
this.mapping[key].reference.setOffset(offset); // Passing glArguments to `new gl[glClass](<arguments>)`
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
} }
load(key, firstLoad = false) { if (firstLoad) {
const mappingKey = this.mapping[key]; mappingKey.reference.init();
const glClass = mappingKey.gl; }
const element = mappingKey.element;
let forceShowList = false;
if (!mappingKey.reference) {
const dl = this.droplab;
const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
// Passing glArguments to `new gl[glClass](<arguments>)` if (this.currentDropdown === 'hint') {
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); // Force the dropdown to show if it was clicked from the hint dropdown
} forceShowList = true;
}
if (firstLoad) { this.updateDropdownOffset(key);
mappingKey.reference.init(); mappingKey.reference.render(firstLoad, forceShowList);
}
if (this.currentDropdown === 'hint') { this.currentDropdown = key;
// Force the dropdown to show if it was clicked from the hint dropdown }
forceShowList = true;
}
this.updateDropdownOffset(key); loadDropdown(dropdownName = '') {
mappingKey.reference.render(firstLoad, forceShowList); let firstLoad = false;
this.currentDropdown = key; if (!this.droplab) {
firstLoad = true;
this.droplab = new DropLab();
} }
loadDropdown(dropdownName = '') { const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
let firstLoad = false; const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
&& this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (!this.droplab) { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
firstLoad = true; const key = match && match.key ? match.key : 'hint';
this.droplab = new DropLab(); this.load(key, firstLoad);
} }
}
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); setDropdown() {
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key const query = gl.DropdownUtils.getSearchQuery(true);
&& this.mapping[match.key]; const { lastToken, searchToken } = this.tokenizer.processTokens(query);
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { if (this.currentDropdown) {
const key = match && match.key ? match.key : 'hint'; this.updateCurrentDropdownOffset();
this.load(key, firstLoad);
}
} }
setDropdown() { if (lastToken === searchToken && lastToken !== null) {
const query = gl.DropdownUtils.getSearchQuery(true); // Token is not fully initialized yet because it has no value
const { lastToken, searchToken } = this.tokenizer.processTokens(query); // Eg. token = 'label:'
if (this.currentDropdown) { const split = lastToken.split(':');
this.updateCurrentDropdownOffset(); const dropdownName = split[0].split(' ').last();
} this.loadDropdown(split.length > 1 ? dropdownName : '');
} else if (lastToken) {
if (lastToken === searchToken && lastToken !== null) { // Token has been initialized into an object because it has a value
// Token is not fully initialized yet because it has no value this.loadDropdown(lastToken.key);
// Eg. token = 'label:' } else {
this.loadDropdown('hint');
const split = lastToken.split(':');
const dropdownName = split[0].split(' ').last();
this.loadDropdown(split.length > 1 ? dropdownName : '');
} else if (lastToken) {
// Token has been initialized into an object because it has a value
this.loadDropdown(lastToken.key);
} else {
this.loadDropdown('hint');
}
} }
}
resetDropdowns() { resetDropdowns() {
if (!this.currentDropdown) { if (!this.currentDropdown) {
return; return;
} }
// Force current dropdown to hide // Force current dropdown to hide
this.mapping[this.currentDropdown].reference.hideDropdown(); this.mapping[this.currentDropdown].reference.hideDropdown();
// Re-Load dropdown // Re-Load dropdown
this.setDropdown(); this.setDropdown();
// Reset filters for current dropdown // Reset filters for current dropdown
this.mapping[this.currentDropdown].reference.resetFilters(); this.mapping[this.currentDropdown].reference.resetFilters();
// Reposition dropdown so that it is aligned with cursor // Reposition dropdown so that it is aligned with cursor
this.updateDropdownOffset(this.currentDropdown); this.updateDropdownOffset(this.currentDropdown);
} }
destroyDroplab() { destroyDroplab() {
this.droplab.destroy(); this.droplab.destroy();
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
})();
...@@ -6,489 +6,487 @@ import RecentSearchesStore from './stores/recent_searches_store'; ...@@ -6,489 +6,487 @@ import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service'; import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
(() => { class FilteredSearchManager {
class FilteredSearchManager { constructor(page) {
constructor(page) { this.container = FilteredSearchContainer.container;
this.container = FilteredSearchContainer.container; this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInputForm = this.filteredSearchInput.form;
this.filteredSearchInputForm = this.filteredSearchInput.form; this.clearSearchButton = this.container.querySelector('.clear-search');
this.clearSearchButton = this.container.querySelector('.clear-search'); this.tokensContainer = this.container.querySelector('.tokens-container');
this.tokensContainer = this.container.querySelector('.tokens-container'); this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
this.recentSearchesStore = new RecentSearchesStore();
this.recentSearchesStore = new RecentSearchesStore(); let recentSearchesKey = 'issue-recent-searches';
let recentSearchesKey = 'issue-recent-searches'; if (page === 'merge_requests') {
if (page === 'merge_requests') { recentSearchesKey = 'merge-request-recent-searches';
recentSearchesKey = 'merge-request-recent-searches'; }
} this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
// Fetch recent searches from localStorage
// Fetch recent searches from localStorage this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() .catch(() => {
.catch(() => { // eslint-disable-next-line no-new
// eslint-disable-next-line no-new new Flash('An error occured while parsing recent searches');
new Flash('An error occured while parsing recent searches'); // Gracefully fail to empty array
// Gracefully fail to empty array return [];
return []; })
}) .then((searches) => {
.then((searches) => { // Put any searches that may have come in before
// Put any searches that may have come in before // we fetched the saved searches ahead of the already saved ones
// we fetched the saved searches ahead of the already saved ones const resultantSearches = this.recentSearchesStore.setRecentSearches(
const resultantSearches = this.recentSearchesStore.setRecentSearches( this.recentSearchesStore.state.recentSearches.concat(searches),
this.recentSearchesStore.state.recentSearches.concat(searches),
);
this.recentSearchesService.save(resultantSearches);
});
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
this.recentSearchesService,
document.querySelector('.js-filtered-search-history-dropdown'),
); );
this.recentSearchesRoot.init(); this.recentSearchesService.save(resultantSearches);
});
this.bindEvents(); if (this.filteredSearchInput) {
this.loadSearchParamsFromURL(); this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager.setDropdown(); this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page);
this.cleanupWrapper = this.cleanup.bind(this); this.recentSearchesRoot = new RecentSearchesRoot(
document.addEventListener('beforeunload', this.cleanupWrapper); this.recentSearchesStore,
} this.recentSearchesService,
} document.querySelector('.js-filtered-search-history-dropdown'),
);
this.recentSearchesRoot.init();
cleanup() { this.bindEvents();
this.unbindEvents(); this.loadSearchParamsFromURL();
document.removeEventListener('beforeunload', this.cleanupWrapper); this.dropdownManager.setDropdown();
if (this.recentSearchesRoot) { this.cleanupWrapper = this.cleanup.bind(this);
this.recentSearchesRoot.destroy(); document.addEventListener('beforeunload', this.cleanupWrapper);
}
} }
}
bindEvents() { cleanup() {
this.handleFormSubmit = this.handleFormSubmit.bind(this); this.unbindEvents();
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); document.removeEventListener('beforeunload', this.cleanupWrapper);
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);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
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);
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);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
unbindEvents() { if (this.recentSearchesRoot) {
this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit); this.recentSearchesRoot.destroy();
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);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
}
checkForBackspace(e) { bindEvents() {
// 8 = Backspace Key this.handleFormSubmit = this.handleFormSubmit.bind(this);
// 46 = Delete Key this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
if (e.keyCode === 8 || e.keyCode === 46) { this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); 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);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
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);
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);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
if (this.filteredSearchInput.value === '' && lastVisualToken) { unbindEvents() {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); this.filteredSearchInputForm.removeEventListener('submit', this.handleFormSubmit);
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); 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);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
// Reposition dropdown so that it is aligned with cursor if (this.filteredSearchInput.value === '' && lastVisualToken) {
this.dropdownManager.updateCurrentDropdownOffset(); this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
} }
}
checkForEnter(e) { // Reposition dropdown so that it is aligned with cursor
if (e.keyCode === 38 || e.keyCode === 40) { this.dropdownManager.updateCurrentDropdownOffset();
const selectionStart = this.filteredSearchInput.selectionStart; }
}
e.preventDefault(); checkForEnter(e) {
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); if (e.keyCode === 38 || e.keyCode === 40) {
} const selectionStart = this.filteredSearchInput.selectionStart;
if (e.keyCode === 13) { e.preventDefault();
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
const dropdownEl = dropdown.element; }
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
e.preventDefault(); if (e.keyCode === 13) {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const dropdownEl = dropdown.element;
const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
if (!activeElements.length) { e.preventDefault();
if (this.isHandledAsync) {
e.stopImmediatePropagation();
this.filteredSearchInput.blur(); if (!activeElements.length) {
this.dropdownManager.resetDropdowns(); if (this.isHandledAsync) {
} else { e.stopImmediatePropagation();
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
}
this.search(); this.filteredSearchInput.blur();
this.dropdownManager.resetDropdowns();
} else {
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
} }
this.search();
} }
} }
}
addInputContainerFocus() { addInputContainerFocus() {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
if (inputContainer) { if (inputContainer) {
inputContainer.classList.add('focus'); inputContainer.classList.add('focus');
}
} }
}
removeInputContainerFocus(e) { removeInputContainerFocus(e) {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
!isElementInStaticFilterDropdown && inputContainer) { !isElementInStaticFilterDropdown && inputContainer) {
inputContainer.classList.remove('focus'); inputContainer.classList.remove('focus');
}
} }
}
static selectToken(e) { static selectToken(e) {
const button = e.target.closest('.selectable'); const button = e.target.closest('.selectable');
if (button) { if (button) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button); gl.FilteredSearchVisualTokens.selectToken(button);
}
} }
}
unselectEditTokens(e) { unselectEditTokens(e) {
const inputContainer = this.container.querySelector('.filtered-search-box'); const inputContainer = this.container.querySelector('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementTokensContainer = e.target.classList.contains('tokens-container'); const isElementTokensContainer = e.target.classList.contains('tokens-container');
if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) { if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
gl.FilteredSearchVisualTokens.moveInputToTheRight(); gl.FilteredSearchVisualTokens.moveInputToTheRight();
this.dropdownManager.resetDropdowns(); this.dropdownManager.resetDropdowns();
}
} }
}
editToken(e) { editToken(e) {
const token = e.target.closest('.js-visual-token'); const token = e.target.closest('.js-visual-token');
if (token) { if (token) {
gl.FilteredSearchVisualTokens.editToken(token); gl.FilteredSearchVisualTokens.editToken(token);
this.tokenChange(); this.tokenChange();
}
} }
}
toggleClearSearchButton() { toggleClearSearchButton() {
const query = gl.DropdownUtils.getSearchQuery(); const query = gl.DropdownUtils.getSearchQuery();
const hidden = 'hidden'; const hidden = 'hidden';
const hasHidden = this.clearSearchButton.classList.contains(hidden); const hasHidden = this.clearSearchButton.classList.contains(hidden);
if (query.length === 0 && !hasHidden) { if (query.length === 0 && !hasHidden) {
this.clearSearchButton.classList.add(hidden); this.clearSearchButton.classList.add(hidden);
} else if (query.length && hasHidden) { } else if (query.length && hasHidden) {
this.clearSearchButton.classList.remove(hidden); this.clearSearchButton.classList.remove(hidden);
}
} }
}
handleInputPlaceholder() { handleInputPlaceholder() {
const query = gl.DropdownUtils.getSearchQuery(); const query = gl.DropdownUtils.getSearchQuery();
const placeholder = 'Search or filter results...'; const placeholder = 'Search or filter results...';
const currentPlaceholder = this.filteredSearchInput.placeholder; const currentPlaceholder = this.filteredSearchInput.placeholder;
if (query.length === 0 && currentPlaceholder !== placeholder) { if (query.length === 0 && currentPlaceholder !== placeholder) {
this.filteredSearchInput.placeholder = placeholder; this.filteredSearchInput.placeholder = placeholder;
} else if (query.length > 0 && currentPlaceholder !== '') { } else if (query.length > 0 && currentPlaceholder !== '') {
this.filteredSearchInput.placeholder = ''; this.filteredSearchInput.placeholder = '';
}
} }
}
removeSelectedToken(e) { removeSelectedToken(e) {
// 8 = Backspace Key // 8 = Backspace Key
// 46 = Delete Key // 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken(); gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder(); this.handleInputPlaceholder();
this.toggleClearSearchButton(); this.toggleClearSearchButton();
}
} }
}
onClearSearch(e) { onClearSearch(e) {
e.preventDefault(); e.preventDefault();
this.clearSearch(); this.clearSearch();
} }
clearSearch() { clearSearch() {
this.filteredSearchInput.value = ''; this.filteredSearchInput.value = '';
const removeElements = []; const removeElements = [];
[].forEach.call(this.tokensContainer.children, (t) => { [].forEach.call(this.tokensContainer.children, (t) => {
if (t.classList.contains('js-visual-token')) { if (t.classList.contains('js-visual-token')) {
removeElements.push(t); removeElements.push(t);
} }
}); });
removeElements.forEach((el) => { removeElements.forEach((el) => {
el.parentElement.removeChild(el); el.parentElement.removeChild(el);
}); });
this.clearSearchButton.classList.add('hidden'); this.clearSearchButton.classList.add('hidden');
this.handleInputPlaceholder(); this.handleInputPlaceholder();
this.dropdownManager.resetDropdowns(); this.dropdownManager.resetDropdowns();
if (this.isHandledAsync) { if (this.isHandledAsync) {
this.search(); this.search();
}
} }
}
handleInputVisualToken() { handleInputVisualToken() {
const input = this.filteredSearchInput; const input = this.filteredSearchInput;
const { tokens, searchToken } const { tokens, searchToken }
= gl.FilteredSearchTokenizer.processTokens(input.value); = gl.FilteredSearchTokenizer.processTokens(input.value);
const { isLastVisualTokenValid } const { isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) { if (isLastVisualTokenValid) {
tokens.forEach((t) => { tokens.forEach((t) => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
}); });
const fragments = searchToken.split(':');
if (fragments.length > 1) {
const inputValues = fragments[0].split(' ');
const tokenKey = inputValues.last();
if (inputValues.length > 1) {
inputValues.pop();
const searchTerms = inputValues.join(' ');
input.value = input.value.replace(searchTerms, '');
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
}
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); const fragments = searchToken.split(':');
input.value = input.value.replace(`${tokenKey}:`, ''); if (fragments.length > 1) {
} const inputValues = fragments[0].split(' ');
} else { const tokenKey = inputValues.last();
// Keep listening to token until we determine that the user is done typing the token value
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { if (inputValues.length > 1) {
gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken); inputValues.pop();
const searchTerms = inputValues.join(' ');
// Trim the last space as seen in the if statement above input.value = input.value.replace(searchTerms, '');
input.value = input.value.replace(searchToken, '').trim(); gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
} }
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
input.value = input.value.replace(`${tokenKey}:`, '');
} }
} } else {
// Keep listening to token until we determine that the user is done typing the token value
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
handleFormSubmit(e) { if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
e.preventDefault(); gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
this.search();
}
saveCurrentSearchQuery() { // Trim the last space as seen in the if statement above
// Don't save before we have fetched the already saved searches input.value = input.value.replace(searchToken, '').trim();
this.fetchingRecentSearchesPromise.then(() => { }
const searchQuery = gl.DropdownUtils.getSearchQuery();
if (searchQuery.length > 0) {
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
this.recentSearchesService.save(resultantSearches);
}
});
} }
}
loadSearchParamsFromURL() { handleFormSubmit(e) {
const params = gl.utils.getUrlParamsArray(); e.preventDefault();
const usernameParams = this.getUsernameParams(); this.search();
let hasFilteredSearch = false; }
params.forEach((p) => { saveCurrentSearchQuery() {
const split = p.split('='); // Don't save before we have fetched the already saved searches
const keyParam = decodeURIComponent(split[0]); this.fetchingRecentSearchesPromise.then(() => {
const value = split[1]; const searchQuery = gl.DropdownUtils.getSearchQuery();
if (searchQuery.length > 0) {
const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
this.recentSearchesService.save(resultantSearches);
}
});
}
// Check if it matches edge conditions listed in this.filteredSearchTokenKeys loadSearchParamsFromURL() {
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false;
if (condition) { params.forEach((p) => {
hasFilteredSearch = true; const split = p.split('=');
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); const keyParam = decodeURIComponent(split[0]);
} else { const value = split[1];
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded + // Check if it matches edge conditions listed in this.filteredSearchTokenKeys
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) {
const indexOf = keyParam.indexOf('_');
const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
const symbol = match.symbol;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
}
if (condition) {
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
} 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) {
const indexOf = keyParam.indexOf('_');
const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
const symbol = match.symbol;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
}
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true; hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
} else if (!match && keyParam === 'assignee_id') { }
const id = parseInt(value, 10); } else if (!match && keyParam === 'author_id') {
if (usernameParams[id]) { const id = parseInt(value, 10);
hasFilteredSearch = true; if (usernameParams[id]) {
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true; hasFilteredSearch = true;
this.filteredSearchInput.value = sanitizedValue; gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
} }
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
this.filteredSearchInput.value = sanitizedValue;
} }
}); }
});
this.saveCurrentSearchQuery(); this.saveCurrentSearchQuery();
if (hasFilteredSearch) { if (hasFilteredSearch) {
this.clearSearchButton.classList.remove('hidden'); this.clearSearchButton.classList.remove('hidden');
this.handleInputPlaceholder(); this.handleInputPlaceholder();
}
} }
}
search() { search() {
const paths = []; const paths = [];
const searchQuery = gl.DropdownUtils.getSearchQuery(); const searchQuery = gl.DropdownUtils.getSearchQuery();
this.saveCurrentSearchQuery();
const { tokens, searchToken } this.saveCurrentSearchQuery();
= this.tokenizer.processTokens(searchQuery);
const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => { const { tokens, searchToken }
const condition = this.filteredSearchTokenKeys = this.tokenizer.processTokens(searchQuery);
.searchByConditionKeyValue(token.key, token.value.toLowerCase()); const currentState = gl.utils.getParameterByName('state') || 'opened';
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; paths.push(`state=${currentState}`);
const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
if (condition) { tokens.forEach((token) => {
tokenPath = condition.url; const condition = this.filteredSearchTokenKeys
} else { .searchByConditionKeyValue(token.key, token.value.toLowerCase());
let tokenValue = token.value; const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || if (condition) {
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { tokenPath = condition.url;
tokenValue = tokenValue.slice(1, tokenValue.length - 1); } else {
} let tokenValue = token.value;
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
} }
paths.push(tokenPath); tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
});
if (searchToken) {
const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
paths.push(`search=${sanitized}`);
} }
const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`; paths.push(tokenPath);
});
if (this.updateObject) { if (searchToken) {
this.updateObject(parameterizedUrl); const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
} else { paths.push(`search=${sanitized}`);
gl.utils.visitUrl(parameterizedUrl);
}
} }
getUsernameParams() { const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
const usernamesById = {};
try { if (this.updateObject) {
const attribute = this.filteredSearchInput.getAttribute('data-username-params'); this.updateObject(parameterizedUrl);
JSON.parse(attribute).forEach((user) => { } else {
usernamesById[user.id] = user.username; gl.utils.visitUrl(parameterizedUrl);
});
} catch (e) {
// do nothing
}
return usernamesById;
} }
}
tokenChange() { getUsernameParams() {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; 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;
}
if (dropdown) { tokenChange() {
const currentDropdownRef = dropdown.reference; const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
this.setDropdownWrapper(); if (dropdown) {
currentDropdownRef.dispatchInputEvent(); const currentDropdownRef = dropdown.reference;
}
}
onrecentSearchesItemSelected(text) { this.setDropdownWrapper();
this.clearSearch(); currentDropdownRef.dispatchInputEvent();
this.filteredSearchInput.value = text;
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search();
} }
} }
window.gl = window.gl || {}; onrecentSearchesItemSelected(text) {
gl.FilteredSearchManager = FilteredSearchManager; this.clearSearch();
})(); this.filteredSearchInput.value = text;
this.filteredSearchInput.dispatchEvent(new CustomEvent('input'));
this.search();
}
}
window.gl = window.gl || {};
gl.FilteredSearchManager = FilteredSearchManager;
(() => { const tokenKeys = [{
const tokenKeys = [{ key: 'author',
key: 'author', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'assignee',
key: 'assignee', type: 'string',
type: 'string', param: 'username',
param: 'username', symbol: '@',
symbol: '@', }, {
}, { key: 'milestone',
key: 'milestone', type: 'string',
type: 'string', param: 'title',
param: 'title', symbol: '%',
symbol: '%', }, {
}, { key: 'label',
key: 'label', type: 'array',
type: 'array', param: 'name[]',
param: 'name[]', symbol: '~',
symbol: '~', }];
}];
const alternativeTokenKeys = [{ const alternativeTokenKeys = [{
key: 'label', key: 'label',
type: 'string', type: 'string',
param: 'name', param: 'name',
symbol: '~', symbol: '~',
}]; }];
const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys);
const conditions = [{ const conditions = [{
url: 'assignee_id=0', url: 'assignee_id=0',
tokenKey: 'assignee', tokenKey: 'assignee',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=No+Milestone', url: 'milestone_title=No+Milestone',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'none', value: 'none',
}, { }, {
url: 'milestone_title=%23upcoming', url: 'milestone_title=%23upcoming',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'upcoming', value: 'upcoming',
}, { }, {
url: 'milestone_title=%23started', url: 'milestone_title=%23started',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'started', value: 'started',
}, { }, {
url: 'label_name[]=No+Label', url: 'label_name[]=No+Label',
tokenKey: 'label', tokenKey: 'label',
value: 'none', value: 'none',
}]; }];
class FilteredSearchTokenKeys { class FilteredSearchTokenKeys {
static get() { static get() {
return tokenKeys; return tokenKeys;
} }
static getAlternatives() { static getAlternatives() {
return alternativeTokenKeys; return alternativeTokenKeys;
} }
static getConditions() { static getConditions() {
return conditions; return conditions;
} }
static searchByKey(key) { static searchByKey(key) {
return tokenKeys.find(tokenKey => tokenKey.key === key) || null; return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
} }
static searchBySymbol(symbol) { static searchBySymbol(symbol) {
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
} }
static searchByKeyParam(keyParam) { static searchByKeyParam(keyParam) {
return tokenKeysWithAlternative.find((tokenKey) => { return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key; let tokenKeyParam = tokenKey.key;
if (tokenKey.param) { if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`; tokenKeyParam += `_${tokenKey.param}`;
} }
return keyParam === tokenKeyParam; return keyParam === tokenKeyParam;
}) || null; }) || null;
} }
static searchByConditionUrl(url) { static searchByConditionUrl(url) {
return conditions.find(condition => condition.url === url) || null; return conditions.find(condition => condition.url === url) || null;
} }
static searchByConditionKeyValue(key, value) { static searchByConditionKeyValue(key, value) {
return conditions return conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null; .find(condition => condition.tokenKey === key && condition.value === value) || null;
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
})();
require('./filtered_search_token_keys'); require('./filtered_search_token_keys');
(() => { class FilteredSearchTokenizer {
class FilteredSearchTokenizer { static processTokens(input) {
static processTokens(input) { const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); // Regex extracts `(token):(symbol)(value)`
// Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single)
// Values that start with a double quote must end in a double quote (same for single) const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokens = [];
const tokens = []; const tokenIndexes = []; // stores key+value for simple search
const tokenIndexes = []; // stores key+value for simple search let lastToken = null;
let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { let tokenValue = v1 || v2 || v3;
let tokenValue = v1 || v2 || v3; let tokenSymbol = symbol;
let tokenSymbol = symbol; let tokenIndex = '';
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { tokenSymbol = tokenValue;
tokenSymbol = tokenValue; tokenValue = '';
tokenValue = '';
}
tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return { tokenIndex = `${key}:${tokenValue}`;
tokens,
lastToken, // Prevent adding duplicates
searchToken, if (tokenIndexes.indexOf(tokenIndex) === -1) {
}; tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
} }
return {
tokens,
lastToken,
searchToken,
};
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
gl.FilteredSearchTokenizer = FilteredSearchTokenizer; gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
})();
...@@ -3,69 +3,67 @@ require('~/filtered_search/filtered_search_tokenizer'); ...@@ -3,69 +3,67 @@ require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown'); require('~/filtered_search/filtered_search_dropdown');
require('~/filtered_search/dropdown_user'); require('~/filtered_search/dropdown_user');
(() => { describe('Dropdown User', () => {
describe('Dropdown User', () => { describe('getSearchInput', () => {
describe('getSearchInput', () => { let dropdownUser;
let dropdownUser;
beforeEach(() => { beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
dropdownUser = new gl.DropdownUser(); dropdownUser = new gl.DropdownUser();
}); });
it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: '"johnny appleseed',
});
expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: '"johnny appleseed',
}); });
it('should not return the single quote found in value', () => { expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ });
lastToken: '\'larry boy',
});
expect(dropdownUser.getSearchInput()).toBe('larry boy'); it('should not return the single quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: '\'larry boy',
}); });
expect(dropdownUser.getSearchInput()).toBe('larry boy');
}); });
});
describe('config AjaxFilter\'s endpoint', () => { describe('config AjaxFilter\'s endpoint', () => {
beforeEach(() => { beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
}); });
it('should return endpoint', () => { it('should return endpoint', () => {
window.gon = { window.gon = {
relative_url_root: '', relative_url_root: '',
}; };
const dropdown = new gl.DropdownUser(); const dropdown = new gl.DropdownUser();
expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
}); });
it('should return endpoint when relative_url_root is undefined', () => { it('should return endpoint when relative_url_root is undefined', () => {
const dropdown = new gl.DropdownUser(); const dropdown = new gl.DropdownUser();
expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json');
}); });
it('should return endpoint with relative url when available', () => { it('should return endpoint with relative url when available', () => {
window.gon = { window.gon = {
relative_url_root: '/gitlab_directory', relative_url_root: '/gitlab_directory',
}; };
const dropdown = new gl.DropdownUser(); const dropdown = new gl.DropdownUser();
expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
}); });
afterEach(() => { afterEach(() => {
window.gon = {}; window.gon = {};
});
}); });
}); });
})(); });
...@@ -3,308 +3,306 @@ require('~/filtered_search/dropdown_utils'); ...@@ -3,308 +3,306 @@ require('~/filtered_search/dropdown_utils');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
(() => { describe('Dropdown Utils', () => {
describe('Dropdown Utils', () => { describe('getEscapedText', () => {
describe('getEscapedText', () => { it('should return same word when it has no space', () => {
it('should return same word when it has no space', () => { const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); expect(escaped).toBe('textWithoutSpace');
expect(escaped).toBe('textWithoutSpace'); });
});
it('should escape with double quotes', () => { it('should escape with double quotes', () => {
let escaped = gl.DropdownUtils.getEscapedText('text with space'); let escaped = gl.DropdownUtils.getEscapedText('text with space');
expect(escaped).toBe('"text with space"'); expect(escaped).toBe('"text with space"');
escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
expect(escaped).toBe('"won\'t fix"'); expect(escaped).toBe('"won\'t fix"');
}); });
it('should escape with single quotes', () => { it('should escape with single quotes', () => {
const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
expect(escaped).toBe('\'won"t fix\''); expect(escaped).toBe('\'won"t fix\'');
}); });
it('should escape with single quotes by default', () => { it('should escape with single quotes by default', () => {
const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
expect(escaped).toBe('\'won"t\' fix\''); expect(escaped).toBe('\'won"t\' fix\'');
});
}); });
});
describe('filterWithSymbol', () => { describe('filterWithSymbol', () => {
let input; let input;
const item = { const item = {
title: '@root', title: '@root',
}; };
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<input type="text" id="test" /> <input type="text" id="test" />
`); `);
input = document.getElementById('test'); input = document.getElementById('test');
}); });
it('should filter without symbol', () => { it('should filter without symbol', () => {
input.value = 'roo'; input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with symbol', () => { it('should filter with symbol', () => {
input.value = '@roo'; input.value = '@roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
describe('filters multiple word title', () => { describe('filters multiple word title', () => {
const multipleWordItem = { const multipleWordItem = {
title: 'Community Contributions', title: 'Community Contributions',
}; };
it('should filter with double quote', () => { it('should filter with double quote', () => {
input.value = '"'; input.value = '"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and symbol', () => { it('should filter with double quote and symbol', () => {
input.value = '~"'; input.value = '~"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and multiple words', () => { it('should filter with double quote and multiple words', () => {
input.value = '"community con'; input.value = '"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote, symbol and multiple words', () => { it('should filter with double quote, symbol and multiple words', () => {
input.value = '~"community con'; input.value = '~"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote', () => { it('should filter with single quote', () => {
input.value = '\''; input.value = '\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and symbol', () => { it('should filter with single quote and symbol', () => {
input.value = '~\''; input.value = '~\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and multiple words', () => { it('should filter with single quote and multiple words', () => {
input.value = '\'community con'; input.value = '\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote, symbol and multiple words', () => { it('should filter with single quote, symbol and multiple words', () => {
input.value = '~\'community con'; input.value = '~\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
});
}); });
}); });
});
describe('filterHint', () => { describe('filterHint', () => {
let input; let input;
beforeEach(() => {
setFixtures(`
<ul class="tokens-container">
<li class="input-token">
<input class="filtered-search" type="text" id="test" />
</li>
</ul>
`);
input = document.getElementById('test');
});
it('should filter', () => { beforeEach(() => {
input.value = 'l'; setFixtures(`
let updatedItem = gl.DropdownUtils.filterHint(input, { <ul class="tokens-container">
hint: 'label', <li class="input-token">
}); <input class="filtered-search" type="text" id="test" />
expect(updatedItem.droplab_hidden).toBe(false); </li>
</ul>
`);
input.value = 'o'; input = document.getElementById('test');
updatedItem = gl.DropdownUtils.filterHint(input, { });
hint: 'label',
});
expect(updatedItem.droplab_hidden).toBe(true);
});
it('should return droplab_hidden false when item has no hint', () => { it('should filter', () => {
const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); input.value = 'l';
expect(updatedItem.droplab_hidden).toBe(false); let updatedItem = gl.DropdownUtils.filterHint(input, {
hint: 'label',
}); });
expect(updatedItem.droplab_hidden).toBe(false);
it('should allow multiple if item.type is array', () => { input.value = 'o';
input.value = 'label:~first la'; updatedItem = gl.DropdownUtils.filterHint(input, {
const updatedItem = gl.DropdownUtils.filterHint(input, { hint: 'label',
hint: 'label',
type: 'array',
});
expect(updatedItem.droplab_hidden).toBe(false);
}); });
expect(updatedItem.droplab_hidden).toBe(true);
});
it('should prevent multiple if item.type is not array', () => { it('should return droplab_hidden false when item has no hint', () => {
input.value = 'milestone:~first mile'; const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
let updatedItem = gl.DropdownUtils.filterHint(input, { expect(updatedItem.droplab_hidden).toBe(false);
hint: 'milestone', });
});
expect(updatedItem.droplab_hidden).toBe(true);
updatedItem = gl.DropdownUtils.filterHint(input, { it('should allow multiple if item.type is array', () => {
hint: 'milestone', input.value = 'label:~first la';
type: 'string', const updatedItem = gl.DropdownUtils.filterHint(input, {
}); hint: 'label',
expect(updatedItem.droplab_hidden).toBe(true); type: 'array',
}); });
expect(updatedItem.droplab_hidden).toBe(false);
}); });
describe('setDataValueIfSelected', () => { it('should prevent multiple if item.type is not array', () => {
beforeEach(() => { input.value = 'milestone:~first mile';
spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') let updatedItem = gl.DropdownUtils.filterHint(input, {
.and.callFake(() => {}); hint: 'milestone',
}); });
expect(updatedItem.droplab_hidden).toBe(true);
it('calls addWordToInput when dataValue exists', () => { updatedItem = gl.DropdownUtils.filterHint(input, {
const selected = { hint: 'milestone',
getAttribute: () => 'value', type: 'string',
};
gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
}); });
expect(updatedItem.droplab_hidden).toBe(true);
});
});
it('returns true when dataValue exists', () => { describe('setDataValueIfSelected', () => {
const selected = { beforeEach(() => {
getAttribute: () => 'value', spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
}; .and.callFake(() => {});
});
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); it('calls addWordToInput when dataValue exists', () => {
expect(result).toBe(true); const selected = {
}); getAttribute: () => 'value',
};
it('returns false when dataValue does not exist', () => { gl.DropdownUtils.setDataValueIfSelected(null, selected);
const selected = { expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
getAttribute: () => null, });
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); it('returns true when dataValue exists', () => {
expect(result).toBe(false); const selected = {
}); getAttribute: () => 'value',
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(result).toBe(true);
}); });
describe('getInputSelectionPosition', () => { it('returns false when dataValue does not exist', () => {
describe('word with trailing spaces', () => { const selected = {
const value = 'label:none '; getAttribute: () => null,
};
const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
expect(result).toBe(false);
});
});
it('should return selectionStart when cursor is at the trailing space', () => { describe('getInputSelectionPosition', () => {
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ describe('word with trailing spaces', () => {
selectionStart: 11, const value = 'label:none ';
value,
});
expect(left).toBe(11); it('should return selectionStart when cursor is at the trailing space', () => {
expect(right).toBe(11); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 11,
value,
}); });
it('should return input when cursor is at the start of input', () => { expect(left).toBe(11);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(11);
selectionStart: 0, });
value,
});
expect(left).toBe(0); it('should return input when cursor is at the start of input', () => {
expect(right).toBe(10); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 0,
value,
}); });
it('should return input when cursor is at the middle of input', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(10);
selectionStart: 7, });
value,
});
expect(left).toBe(0); it('should return input when cursor is at the middle of input', () => {
expect(right).toBe(10); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 7,
value,
}); });
it('should return input when cursor is at the end of input', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(10);
selectionStart: 10, });
value,
});
expect(left).toBe(0); it('should return input when cursor is at the end of input', () => {
expect(right).toBe(10); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 10,
value,
}); });
});
describe('multiple words', () => { expect(left).toBe(0);
const value = 'label:~"Community Contribution"'; expect(right).toBe(10);
});
});
it('should return input when cursor is after the first word', () => { describe('multiple words', () => {
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ const value = 'label:~"Community Contribution"';
selectionStart: 17,
value,
});
expect(left).toBe(0); it('should return input when cursor is after the first word', () => {
expect(right).toBe(31); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 17,
value,
}); });
it('should return input when cursor is before the second word', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(31);
selectionStart: 18, });
value,
});
expect(left).toBe(0); it('should return input when cursor is before the second word', () => {
expect(right).toBe(31); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 18,
value,
}); });
});
describe('incomplete multiple words', () => { expect(left).toBe(0);
const value = 'label:~"Community Contribution'; expect(right).toBe(31);
});
});
it('should return entire input when cursor is at the start of input', () => { describe('incomplete multiple words', () => {
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ const value = 'label:~"Community Contribution';
selectionStart: 0,
value,
});
expect(left).toBe(0); it('should return entire input when cursor is at the start of input', () => {
expect(right).toBe(30); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 0,
value,
}); });
it('should return entire input when cursor is at the end of input', () => { expect(left).toBe(0);
const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ expect(right).toBe(30);
selectionStart: 30, });
value,
});
expect(left).toBe(0); it('should return entire input when cursor is at the end of input', () => {
expect(right).toBe(30); const { left, right } = gl.DropdownUtils.getInputSelectionPosition({
selectionStart: 30,
value,
}); });
expect(left).toBe(0);
expect(right).toBe(30);
}); });
}); });
}); });
})(); });
...@@ -3,99 +3,97 @@ require('~/filtered_search/filtered_search_visual_tokens'); ...@@ -3,99 +3,97 @@ require('~/filtered_search/filtered_search_visual_tokens');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
(() => { describe('Filtered Search Dropdown Manager', () => {
describe('Filtered Search Dropdown Manager', () => { describe('addWordToInput', () => {
describe('addWordToInput', () => { function getInputValue() {
function getInputValue() { return document.querySelector('.filtered-search').value;
return document.querySelector('.filtered-search').value; }
}
function setInputValue(value) {
function setInputValue(value) { document.querySelector('.filtered-search').value = value;
document.querySelector('.filtered-search').value = value; }
}
beforeEach(() => {
beforeEach(() => { setFixtures(`
setFixtures(` <ul class="tokens-container">
<ul class="tokens-container"> <li class="input-token">
<li class="input-token"> <input class="filtered-search">
<input class="filtered-search"> </li>
</li> </ul>
</ul> `);
`); });
});
describe('input has no existing value', () => { describe('input has no existing value', () => {
it('should add just tokenName', () => { it('should add just tokenName', () => {
gl.FilteredSearchDropdownManager.addWordToInput('milestone'); gl.FilteredSearchDropdownManager.addWordToInput('milestone');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('milestone'); expect(token.querySelector('.name').innerText).toBe('milestone');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should add tokenName and tokenValue', () => { it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label'); gl.FilteredSearchDropdownManager.addWordToInput('label');
let token = document.querySelector('.tokens-container .js-visual-token'); let token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
// We have to get that reference again // We have to get that reference again
// Because gl.FilteredSearchDropdownManager deletes the previous token // Because gl.FilteredSearchDropdownManager deletes the previous token
token = document.querySelector('.tokens-container .js-visual-token'); token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('none'); expect(token.querySelector('.value').innerText).toBe('none');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
});
}); });
});
describe('input has existing value', () => { describe('input has existing value', () => {
it('should be able to just add tokenName', () => { it('should be able to just add tokenName', () => {
setInputValue('a'); setInputValue('a');
gl.FilteredSearchDropdownManager.addWordToInput('author'); gl.FilteredSearchDropdownManager.addWordToInput('author');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author'); expect(token.querySelector('.name').innerText).toBe('author');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should replace tokenValue', () => { it('should replace tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('author'); gl.FilteredSearchDropdownManager.addWordToInput('author');
setInputValue('roo'); setInputValue('roo');
gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author'); expect(token.querySelector('.name').innerText).toBe('author');
expect(token.querySelector('.value').innerText).toBe('@root'); expect(token.querySelector('.value').innerText).toBe('@root');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should add tokenValues containing spaces', () => { it('should add tokenValues containing spaces', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label'); gl.FilteredSearchDropdownManager.addWordToInput('label');
setInputValue('"test '); setInputValue('"test ');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
});
}); });
}); });
}); });
})(); });
...@@ -6,271 +6,269 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -6,271 +6,269 @@ require('~/filtered_search/filtered_search_dropdown_manager');
require('~/filtered_search/filtered_search_manager'); require('~/filtered_search/filtered_search_manager');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
(() => { describe('Filtered Search Manager', () => {
describe('Filtered Search Manager', () => { let input;
let input; let manager;
let manager; let tokensContainer;
let tokensContainer; const placeholder = 'Search or filter results...';
const placeholder = 'Search or filter results...';
function dispatchBackspaceEvent(element, eventType) {
function dispatchBackspaceEvent(element, eventType) { const backspaceKey = 8;
const backspaceKey = 8; const event = new Event(eventType);
const event = new Event(eventType); event.keyCode = backspaceKey;
event.keyCode = backspaceKey; element.dispatchEvent(event);
element.dispatchEvent(event); }
}
function dispatchDeleteEvent(element, eventType) {
const deleteKey = 46;
const event = new Event(eventType);
event.keyCode = deleteKey;
element.dispatchEvent(event);
}
beforeEach(() => {
setFixtures(`
<div class="filtered-search-box">
<form>
<ul class="tokens-container list-unstyled">
${FilteredSearchSpecHelper.createInputHTML(placeholder)}
</ul>
<button class="clear-search" type="button">
<i class="fa fa-times"></i>
</button>
</form>
</div>
`);
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager();
});
function dispatchDeleteEvent(element, eventType) { afterEach(() => {
const deleteKey = 46; manager.cleanup();
const event = new Event(eventType); });
event.keyCode = deleteKey;
element.dispatchEvent(event);
}
beforeEach(() => { describe('search', () => {
setFixtures(` const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
<div class="filtered-search-box">
<form>
<ul class="tokens-container list-unstyled">
${FilteredSearchSpecHelper.createInputHTML(placeholder)}
</ul>
<button class="clear-search" type="button">
<i class="fa fa-times"></i>
</button>
</form>
</div>
`);
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); it('should search with a single word', (done) => {
spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); input.value = 'searchTerm';
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
input = document.querySelector('.filtered-search'); spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
tokensContainer = document.querySelector('.tokens-container'); expect(url).toEqual(`${defaultParams}&search=searchTerm`);
manager = new gl.FilteredSearchManager(); done();
}); });
afterEach(() => { manager.search();
manager.cleanup();
}); });
describe('search', () => { it('should search with multiple words', (done) => {
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; input.value = 'awesome search terms';
it('should search with a single word', (done) => {
input.value = 'searchTerm';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`); expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done(); done();
});
manager.search();
}); });
it('should search with multiple words', (done) => { manager.search();
input.value = 'awesome search terms'; });
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { it('should search with special characters', (done) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); input.value = '~!@#$%^&*()_+{}:<>,.?/';
done();
});
manager.search(); spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
}); });
it('should search with special characters', (done) => { manager.search();
input.value = '~!@#$%^&*()_+{}:<>,.?/'; });
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { it('removes duplicated tokens', (done) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
done(); ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
}); ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
manager.search(); spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
}); });
it('removes duplicated tokens', (done) => { manager.search();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` });
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} });
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
manager.search(); describe('handleInputPlaceholder', () => {
}); it('should render placeholder when there is no input', () => {
expect(input.placeholder).toEqual(placeholder);
}); });
describe('handleInputPlaceholder', () => { it('should not render placeholder when there is input', () => {
it('should render placeholder when there is no input', () => { input.value = 'test words';
expect(input.placeholder).toEqual(placeholder);
}); const event = new Event('input');
input.dispatchEvent(event);
it('should not render placeholder when there is input', () => { expect(input.placeholder).toEqual('');
input.value = 'test words'; });
const event = new Event('input'); it('should not render placeholder when there are tokens and no input', () => {
input.dispatchEvent(event); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
expect(input.placeholder).toEqual(''); const event = new Event('input');
}); input.dispatchEvent(event);
it('should not render placeholder when there are tokens and no input', () => { expect(input.placeholder).toEqual('');
});
});
describe('checkForBackspace', () => {
describe('tokens and no input', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
); );
const event = new Event('input');
input.dispatchEvent(event);
expect(input.placeholder).toEqual('');
}); });
});
describe('checkForBackspace', () => {
describe('tokens and no input', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
});
it('removes last token', () => { it('removes last token', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
dispatchBackspaceEvent(input, 'keyup'); dispatchBackspaceEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
});
it('sets the input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
dispatchDeleteEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
expect(input.value).toEqual('~bug');
});
}); });
it('does not remove token or change input when there is existing input', () => { it('sets the input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
input.value = 'text';
dispatchDeleteEvent(input, 'keyup'); dispatchDeleteEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('~bug');
expect(input.value).toEqual('text');
}); });
}); });
describe('removeSelectedToken', () => { it('does not remove token or change input when there is existing input', () => {
function getVisualTokens() { spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
return tokensContainer.querySelectorAll('.js-visual-token'); spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
}
beforeEach(() => { input.value = 'text';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( dispatchDeleteEvent(input, 'keyup');
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
});
it('removes selected token when the backspace key is pressed', () => { expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
expect(getVisualTokens().length).toEqual(1); expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
expect(input.value).toEqual('text');
});
});
dispatchBackspaceEvent(document, 'keydown'); describe('removeSelectedToken', () => {
function getVisualTokens() {
return tokensContainer.querySelectorAll('.js-visual-token');
}
expect(getVisualTokens().length).toEqual(0); beforeEach(() => {
}); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
});
it('removes selected token when the delete key is pressed', () => { it('removes selected token when the backspace key is pressed', () => {
expect(getVisualTokens().length).toEqual(1); expect(getVisualTokens().length).toEqual(1);
dispatchDeleteEvent(document, 'keydown'); dispatchBackspaceEvent(document, 'keydown');
expect(getVisualTokens().length).toEqual(0); expect(getVisualTokens().length).toEqual(0);
}); });
it('updates the input placeholder after removal', () => { it('removes selected token when the delete key is pressed', () => {
manager.handleInputPlaceholder(); expect(getVisualTokens().length).toEqual(1);
expect(input.placeholder).toEqual(''); dispatchDeleteEvent(document, 'keydown');
expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown'); expect(getVisualTokens().length).toEqual(0);
});
expect(input.placeholder).not.toEqual(''); it('updates the input placeholder after removal', () => {
expect(getVisualTokens().length).toEqual(0); manager.handleInputPlaceholder();
});
it('updates the clear button after removal', () => { expect(input.placeholder).toEqual('');
manager.toggleClearSearchButton(); expect(getVisualTokens().length).toEqual(1);
const clearButton = document.querySelector('.clear-search'); dispatchBackspaceEvent(document, 'keydown');
expect(clearButton.classList.contains('hidden')).toEqual(false); expect(input.placeholder).not.toEqual('');
expect(getVisualTokens().length).toEqual(1); expect(getVisualTokens().length).toEqual(0);
});
dispatchBackspaceEvent(document, 'keydown'); it('updates the clear button after removal', () => {
manager.toggleClearSearchButton();
expect(clearButton.classList.contains('hidden')).toEqual(true); const clearButton = document.querySelector('.clear-search');
expect(getVisualTokens().length).toEqual(0);
}); expect(clearButton.classList.contains('hidden')).toEqual(false);
expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
expect(clearButton.classList.contains('hidden')).toEqual(true);
expect(getVisualTokens().length).toEqual(0);
}); });
});
describe('unselects token', () => { describe('unselects token', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
`); `);
}); });
it('unselects token when input is clicked', () => { it('unselects token when input is clicked', () => {
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true); expect(selectedToken.classList.contains('selected')).toEqual(true);
expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
// Click directly on input attached to document // Click directly on input attached to document
// so that the click event will propagate properly // so that the click event will propagate properly
document.querySelector('.filtered-search').click(); document.querySelector('.filtered-search').click();
expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
expect(selectedToken.classList.contains('selected')).toEqual(false); expect(selectedToken.classList.contains('selected')).toEqual(false);
}); });
it('unselects token when document.body is clicked', () => { it('unselects token when document.body is clicked', () => {
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true); expect(selectedToken.classList.contains('selected')).toEqual(true);
expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
document.body.click(); document.body.click();
expect(selectedToken.classList.contains('selected')).toEqual(false); expect(selectedToken.classList.contains('selected')).toEqual(false);
expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
});
}); });
});
describe('toggleInputContainerFocus', () => { describe('toggleInputContainerFocus', () => {
it('toggles on focus', () => { it('toggles on focus', () => {
input.focus(); input.focus();
expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true); expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(true);
}); });
it('toggles on blur', () => { it('toggles on blur', () => {
input.blur(); input.blur();
expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false); expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(false);
});
}); });
}); });
})(); });
require('~/extensions/array'); require('~/extensions/array');
require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_token_keys');
(() => { describe('Filtered Search Token Keys', () => {
describe('Filtered Search Token Keys', () => { describe('get', () => {
describe('get', () => { let tokenKeys;
let tokenKeys;
beforeEach(() => {
beforeEach(() => { tokenKeys = gl.FilteredSearchTokenKeys.get();
tokenKeys = gl.FilteredSearchTokenKeys.get(); });
});
it('should return tokenKeys', () => {
it('should return tokenKeys', () => { expect(tokenKeys !== null).toBe(true);
expect(tokenKeys !== null).toBe(true); });
});
it('should return tokenKeys as an array', () => {
it('should return tokenKeys as an array', () => { expect(tokenKeys instanceof Array).toBe(true);
expect(tokenKeys instanceof Array).toBe(true); });
}); });
});
describe('getConditions', () => {
describe('getConditions', () => { let conditions;
let conditions;
beforeEach(() => {
beforeEach(() => { conditions = gl.FilteredSearchTokenKeys.getConditions();
conditions = gl.FilteredSearchTokenKeys.getConditions(); });
});
it('should return conditions', () => {
it('should return conditions', () => { expect(conditions !== null).toBe(true);
expect(conditions !== null).toBe(true); });
});
it('should return conditions as an array', () => {
it('should return conditions as an array', () => { expect(conditions instanceof Array).toBe(true);
expect(conditions instanceof Array).toBe(true); });
}); });
});
describe('searchByKey', () => {
describe('searchByKey', () => { it('should return null when key not found', () => {
it('should return null when key not found', () => { const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); expect(tokenKey === null).toBe(true);
expect(tokenKey === null).toBe(true); });
});
it('should return tokenKey when found by key', () => {
it('should return tokenKey when found by key', () => { const tokenKeys = gl.FilteredSearchTokenKeys.get();
const tokenKeys = gl.FilteredSearchTokenKeys.get(); const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
}); });
});
describe('searchBySymbol', () => {
describe('searchBySymbol', () => { it('should return null when symbol not found', () => {
it('should return null when symbol not found', () => { const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); expect(tokenKey === null).toBe(true);
expect(tokenKey === null).toBe(true); });
});
it('should return tokenKey when found by symbol', () => {
it('should return tokenKey when found by symbol', () => { const tokenKeys = gl.FilteredSearchTokenKeys.get();
const tokenKeys = gl.FilteredSearchTokenKeys.get(); const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
}); });
});
describe('searchByKeyParam', () => {
describe('searchByKeyParam', () => { it('should return null when key param not found', () => {
it('should return null when key param not found', () => { const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); expect(tokenKey === null).toBe(true);
expect(tokenKey === null).toBe(true); });
});
it('should return tokenKey when found by key param', () => {
it('should return tokenKey when found by key param', () => { const tokenKeys = gl.FilteredSearchTokenKeys.get();
const tokenKeys = gl.FilteredSearchTokenKeys.get(); const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
});
it('should return alternative tokenKey when found by key param', () => {
it('should return alternative tokenKey when found by key param', () => { const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives();
const tokenKeys = gl.FilteredSearchTokenKeys.getAlternatives(); const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]);
expect(result).toEqual(tokenKeys[0]); });
}); });
});
describe('searchByConditionUrl', () => {
describe('searchByConditionUrl', () => { it('should return null when condition url not found', () => {
it('should return null when condition url not found', () => { const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); expect(condition === null).toBe(true);
expect(condition === null).toBe(true); });
});
it('should return condition when found by url', () => {
it('should return condition when found by url', () => { const conditions = gl.FilteredSearchTokenKeys.getConditions();
const conditions = gl.FilteredSearchTokenKeys.getConditions(); const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); expect(result).toBe(conditions[0]);
expect(result).toBe(conditions[0]); });
}); });
});
describe('searchByConditionKeyValue', () => {
describe('searchByConditionKeyValue', () => { it('should return null when condition tokenKey and value not found', () => {
it('should return null when condition tokenKey and value not found', () => { const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); expect(condition === null).toBe(true);
expect(condition === null).toBe(true); });
});
it('should return condition when found by tokenKey and value', () => {
it('should return condition when found by tokenKey and value', () => { const conditions = gl.FilteredSearchTokenKeys.getConditions();
const conditions = gl.FilteredSearchTokenKeys.getConditions(); const result = gl.FilteredSearchTokenKeys
const result = gl.FilteredSearchTokenKeys .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
.searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); expect(result).toEqual(conditions[0]);
expect(result).toEqual(conditions[0]);
});
}); });
}); });
})(); });
...@@ -2,134 +2,132 @@ require('~/extensions/array'); ...@@ -2,134 +2,132 @@ require('~/extensions/array');
require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_token_keys');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
(() => { describe('Filtered Search Tokenizer', () => {
describe('Filtered Search Tokenizer', () => { describe('processTokens', () => {
describe('processTokens', () => { it('returns for input containing only search value', () => {
it('returns for input containing only search value', () => { const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); expect(results.searchToken).toBe('searchTerm');
expect(results.searchToken).toBe('searchTerm'); expect(results.tokens.length).toBe(0);
expect(results.tokens.length).toBe(0); expect(results.lastToken).toBe(results.searchToken);
expect(results.lastToken).toBe(results.searchToken); });
});
it('returns for input containing only tokens', () => {
it('returns for input containing only tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
.processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); expect(results.searchToken).toBe('');
expect(results.searchToken).toBe(''); expect(results.tokens.length).toBe(4);
expect(results.tokens.length).toBe(4); expect(results.tokens[3]).toBe(results.lastToken);
expect(results.tokens[3]).toBe(results.lastToken);
expect(results.tokens[0].key).toBe('author');
expect(results.tokens[0].key).toBe('author'); expect(results.tokens[0].value).toBe('root');
expect(results.tokens[0].value).toBe('root'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[1].key).toBe('label');
expect(results.tokens[1].key).toBe('label'); expect(results.tokens[1].value).toBe('"Very Important"');
expect(results.tokens[1].value).toBe('"Very Important"'); expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[2].key).toBe('milestone');
expect(results.tokens[2].key).toBe('milestone'); expect(results.tokens[2].value).toBe('v1.0');
expect(results.tokens[2].value).toBe('v1.0'); expect(results.tokens[2].symbol).toBe('%');
expect(results.tokens[2].symbol).toBe('%');
expect(results.tokens[3].key).toBe('assignee');
expect(results.tokens[3].key).toBe('assignee'); expect(results.tokens[3].value).toBe('none');
expect(results.tokens[3].value).toBe('none'); expect(results.tokens[3].symbol).toBe('');
expect(results.tokens[3].symbol).toBe(''); });
});
it('returns for input starting with search value and ending with tokens', () => {
it('returns for input starting with search value and ending with tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('searchTerm anotherSearchTerm milestone:none');
.processTokens('searchTerm anotherSearchTerm milestone:none'); expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(1);
expect(results.tokens.length).toBe(1); expect(results.tokens[0]).toBe(results.lastToken);
expect(results.tokens[0]).toBe(results.lastToken); expect(results.tokens[0].key).toBe('milestone');
expect(results.tokens[0].key).toBe('milestone'); expect(results.tokens[0].value).toBe('none');
expect(results.tokens[0].value).toBe('none'); expect(results.tokens[0].symbol).toBe('');
expect(results.tokens[0].symbol).toBe(''); });
});
it('returns for input starting with tokens and ending with search value', () => {
it('returns for input starting with tokens and ending with search value', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('assignee:@user searchTerm');
.processTokens('assignee:@user searchTerm');
expect(results.searchToken).toBe('searchTerm');
expect(results.searchToken).toBe('searchTerm'); expect(results.tokens.length).toBe(1);
expect(results.tokens.length).toBe(1); expect(results.tokens[0].key).toBe('assignee');
expect(results.tokens[0].key).toBe('assignee'); expect(results.tokens[0].value).toBe('user');
expect(results.tokens[0].value).toBe('user'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@'); expect(results.lastToken).toBe(results.searchToken);
expect(results.lastToken).toBe(results.searchToken); });
});
it('returns for input containing search value wrapped between tokens', () => {
it('returns for input containing search value wrapped between tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
.processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3);
expect(results.tokens.length).toBe(3); expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[0].key).toBe('author');
expect(results.tokens[0].key).toBe('author'); expect(results.tokens[0].value).toBe('root');
expect(results.tokens[0].value).toBe('root'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[1].key).toBe('label');
expect(results.tokens[1].key).toBe('label'); expect(results.tokens[1].value).toBe('"Won\'t fix"');
expect(results.tokens[1].value).toBe('"Won\'t fix"'); expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[1].symbol).toBe('~');
expect(results.tokens[2].key).toBe('milestone');
expect(results.tokens[2].key).toBe('milestone'); expect(results.tokens[2].value).toBe('none');
expect(results.tokens[2].value).toBe('none'); expect(results.tokens[2].symbol).toBe('');
expect(results.tokens[2].symbol).toBe(''); });
});
it('returns for input containing search value in between tokens', () => {
it('returns for input containing search value in between tokens', () => { const results = gl.FilteredSearchTokenizer
const results = gl.FilteredSearchTokenizer .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
.processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3);
expect(results.tokens.length).toBe(3); expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[2]).toBe(results.lastToken);
expect(results.tokens[0].key).toBe('author');
expect(results.tokens[0].key).toBe('author'); expect(results.tokens[0].value).toBe('root');
expect(results.tokens[0].value).toBe('root'); expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[0].symbol).toBe('@');
expect(results.tokens[1].key).toBe('assignee');
expect(results.tokens[1].key).toBe('assignee'); expect(results.tokens[1].value).toBe('none');
expect(results.tokens[1].value).toBe('none'); expect(results.tokens[1].symbol).toBe('');
expect(results.tokens[1].symbol).toBe('');
expect(results.tokens[2].key).toBe('label');
expect(results.tokens[2].key).toBe('label'); expect(results.tokens[2].value).toBe('Doing');
expect(results.tokens[2].value).toBe('Doing'); expect(results.tokens[2].symbol).toBe('~');
expect(results.tokens[2].symbol).toBe('~'); });
});
it('returns search value for invalid tokens', () => {
it('returns search value for invalid tokens', () => { const results = gl.FilteredSearchTokenizer.processTokens('fake:token');
const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); expect(results.lastToken).toBe('fake:token');
expect(results.lastToken).toBe('fake:token'); expect(results.searchToken).toBe('fake:token');
expect(results.searchToken).toBe('fake:token'); expect(results.tokens.length).toEqual(0);
expect(results.tokens.length).toEqual(0); });
});
it('returns search value and token for mix of valid and invalid tokens', () => {
it('returns search value and token for mix of valid and invalid tokens', () => { const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token');
const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); expect(results.tokens.length).toEqual(1);
expect(results.tokens.length).toEqual(1); expect(results.tokens[0].key).toBe('label');
expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('real');
expect(results.tokens[0].value).toBe('real'); expect(results.tokens[0].symbol).toBe('');
expect(results.tokens[0].symbol).toBe(''); expect(results.lastToken).toBe('fake:token');
expect(results.lastToken).toBe('fake:token'); expect(results.searchToken).toBe('fake:token');
expect(results.searchToken).toBe('fake:token'); });
});
it('returns search value for invalid symbols', () => {
it('returns search value for invalid symbols', () => { const results = gl.FilteredSearchTokenizer.processTokens('std::includes');
const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); expect(results.lastToken).toBe('std::includes');
expect(results.lastToken).toBe('std::includes'); expect(results.searchToken).toBe('std::includes');
expect(results.searchToken).toBe('std::includes'); });
});
it('removes duplicated values', () => {
it('removes duplicated values', () => { const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo');
const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); expect(results.tokens.length).toBe(1);
expect(results.tokens.length).toBe(1); expect(results.tokens[0].key).toBe('label');
expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('foo');
expect(results.tokens[0].value).toBe('foo'); expect(results.tokens[0].symbol).toBe('~');
expect(results.tokens[0].symbol).toBe('~');
});
}); });
}); });
})(); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment