Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClement Ho <ClemMakesApps@gmail.com>2017-01-31 01:53:18 +0300
committerClement Ho <ClemMakesApps@gmail.com>2017-03-08 08:10:32 +0300
commitf44fb5cfd0cc4baada4d88f9724c74fc44326637 (patch)
treee2ff8e5e9b0d14b87aad353abffdc9b7979d6a29 /app/assets/javascripts
parentb5cb1115f4e3357118465ea4becf031b4ea598a6 (diff)
Add filtered search visual tokens
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js19
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js71
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js51
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js174
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js200
9 files changed, 452 insertions, 79 deletions
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index 5cdf11c6a2c..f61be741b4a 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -37,11 +37,14 @@ require('../window')(function(w){
}
}
- self.hook.list[config.method].call(self.hook.list, data);
+ if (!self.destroyed) {
+ self.hook.list[config.method].call(self.hook.list, data);
+ }
},
init: function init(hook) {
var self = this;
+ self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.droplabAjax;
this.hook = hook;
@@ -79,6 +82,7 @@ require('../window')(function(w){
destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ this.destroyed = true;
if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 9e92d544bef..38ff3fb7158 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -28,6 +28,23 @@ require('./filtered_search_dropdown');
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) {
+ // Get previous input values in the input field and convert them into visual tokens
+ const previousInputValues = this.input.value.split(' ');
+ const searchTerms = [];
+
+ previousInputValues.forEach((value, index) => {
+ searchTerms.push(value);
+
+ if (index === previousInputValues.length - 1
+ && token.indexOf(value.toLowerCase()) !== -1) {
+ searchTerms.pop();
+ }
+ });
+
+ if (searchTerms.length > 0) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
+ }
+
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
}
this.dismissDropdown();
@@ -39,7 +56,7 @@ require('./filtered_search_dropdown');
renderContent() {
const dropdownData = [];
- [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
+ [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag } = dropdownMenu.dataset;
if (icon && hint && tag) {
dropdownData.push({
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 7e9c6f74aa5..04e2afad02f 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- let value = lastToken.value || '';
+
+ let value = lastToken || '';
+
+ if (value[0] === '@') {
+ value = value.slice(1);
+ }
// Removes the first character if it is a quotation so that we can search
// with multiple words
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index de3fa116717..14f0d0d0ff2 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -22,38 +22,40 @@
static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
- const query = gl.DropdownUtils.getSearchInput(input);
- const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ const searchInput = gl.DropdownUtils.getSearchInput(input);
- if (lastToken !== searchToken) {
- const title = updatedItem.title.toLowerCase();
- let value = lastToken.value.toLowerCase();
+ const title = updatedItem.title.toLowerCase();
+ let value = searchInput.toLowerCase();
+ let symbol = '';
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
- value = value.slice(1);
- }
-
- // Eg. filterSymbol = ~ for labels
- const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
- const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
+ // Remove the symbol for filter
+ if (value[0] === filterSymbol) {
+ symbol = value[0];
+ value = value.slice(1);
+ }
- updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
- } else {
- updatedItem.droplab_hidden = false;
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
+ value = value.slice(1);
}
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${symbol}${value}`) !== -1;
+
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+
return updatedItem;
}
static filterHint(input, item) {
const updatedItem = item;
- const query = gl.DropdownUtils.getSearchInput(input);
- let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ const searchInput = gl.DropdownUtils.getSearchInput(input);
+ let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput);
lastToken = lastToken.key || lastToken || '';
- if (!lastToken || query.split('').last() === ' ') {
+ if (!lastToken || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastToken) {
const split = lastToken.split(':');
@@ -70,13 +72,40 @@
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
- gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
}
// Return boolean based on whether it was set
return dataValue !== null;
}
+ static getSearchQuery() {
+ const tokensContainer = document.querySelector('.tokens-container');
+ const values = [];
+
+ [].forEach.call(tokensContainer.querySelectorAll('.js-visual-token'), (token) => {
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+ 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);
+ }
+ });
+
+ const input = document.querySelector('.filtered-search');
+ values.push(input && input.value);
+
+ return values.join(' ');
+ }
+
static getSearchInput(filteredSearchInput) {
const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
index faaba994f46..856eb6590ee 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -7,3 +7,4 @@ require('./filtered_search_dropdown');
require('./filtered_search_manager');
require('./filtered_search_token_keys');
require('./filtered_search_tokenizer');
+require('./filtered_search_visual_tokens');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index dd565da507e..134bdc6ad80 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -35,7 +35,7 @@
if (!dataValueSet) {
const value = getValueFunction(selected);
- gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.dismissDropdown();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cecd3518ce3..608c65c78a4 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -58,35 +58,15 @@
};
}
- static addWordToInput(tokenName, tokenValue = '') {
+ static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = document.querySelector('.filtered-search');
- const inputValue = input.value;
- const word = `${tokenName}:${tokenValue}`;
- // Get the string to replace
- let newCaretPosition = input.selectionStart;
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
+ input.value = '';
- input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
-
- // If we have added a tokenValue at the end of the input,
- // add a space and set selection to the end
- if (right >= inputValue.length && tokenValue !== '') {
- input.value += ' ';
- newCaretPosition = input.value.length;
+ if (clicked) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
}
-
- gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
- }
-
- static updateInputCaretPosition(selectionStart, input) {
- // Reset the position
- // Sometimes can end up at end of input
- input.setSelectionRange(selectionStart, selectionStart);
-
- const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
-
- input.setSelectionRange(right, right);
}
updateCurrentDropdownOffset() {
@@ -94,19 +74,14 @@
}
updateDropdownOffset(key) {
- if (!this.font) {
- this.font = window.getComputedStyle(this.filteredSearchInput).font;
- }
-
- const input = this.filteredSearchInput;
- const inputText = input.value.slice(0, input.selectionStart);
- const filterIconPadding = 27;
- let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
+ // Always align dropdown with the input field
+ let offset = this.filteredSearchInput.getBoundingClientRect().left - document.querySelector('.scroll-container').getBoundingClientRect().left;
- const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 :
- this.mapping[key].element.clientWidth;
- const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
+ const maxInputWidth = 240;
+ const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
+ // Make sure offset never exceeds the input container
+ const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
@@ -164,8 +139,8 @@
}
setDropdown() {
- const { lastToken, searchToken } = this.tokenizer
- .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
+ const query = gl.DropdownUtils.getSearchQuery();
+ const { lastToken, searchToken } = this.tokenizer.processTokens(query);
if (this.currentDropdown) {
this.updateCurrentDropdownOffset();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index bbafead0305..4b5f9618d65 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -3,6 +3,7 @@
constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search');
+ this.tokensContainer = document.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) {
@@ -27,36 +28,61 @@
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
+ this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
+ this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
+ this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.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.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.addEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
+ document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('keydown', this.removeSelectedTokenWrapper);
}
unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.removeEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
+ document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (this.filteredSearchInput.value === '' && lastVisualToken) {
+ this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ }
+
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
}
@@ -86,11 +112,67 @@
}
}
- toggleClearSearchButton(e) {
- if (e.target.value) {
- this.clearSearchButton.classList.remove('hidden');
- } else {
- this.clearSearchButton.classList.add('hidden');
+ static selectToken(e) {
+ const button = e.target.closest('.selectable');
+
+ if (button) {
+ e.preventDefault();
+ e.stopPropagation();
+ gl.FilteredSearchVisualTokens.selectToken(button);
+ }
+ }
+
+ unselectEditTokens(e) {
+ const inputContainer = document.querySelector('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementTokensContainer = e.target.classList.contains('tokens-container');
+
+ if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ this.dropdownManager.resetDropdowns();
+ }
+ }
+
+ static editToken(e) {
+ const token = e.target.closest('.js-visual-token');
+
+ if (token) {
+ gl.FilteredSearchVisualTokens.editToken(token);
+ }
+ }
+
+ toggleClearSearchButton() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const hidden = 'hidden';
+ const hasHidden = this.clearSearchButton.classList.contains(hidden);
+
+ if (query.length === 0 && !hasHidden) {
+ this.clearSearchButton.classList.add(hidden);
+ } else if (query.length && hasHidden) {
+ this.clearSearchButton.classList.remove(hidden);
+ }
+ }
+
+ handleInputPlaceholder() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const placeholder = 'Search or filter results...';
+ const currentPlaceholder = this.filteredSearchInput.placeholder;
+
+ if (query.length === 0 && currentPlaceholder !== placeholder) {
+ this.filteredSearchInput.placeholder = placeholder;
+ } else if (query.length > 0 && currentPlaceholder !== '') {
+ this.filteredSearchInput.placeholder = '';
+ }
+ }
+
+ removeSelectedToken(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+ this.handleInputPlaceholder();
+ this.toggleClearSearchButton();
}
}
@@ -98,11 +180,67 @@
e.preventDefault();
this.filteredSearchInput.value = '';
+
+ const removeElements = [];
+
+ [].forEach.call(this.tokensContainer.children, (t) => {
+ if (t.classList.contains('js-visual-token')) {
+ removeElements.push(t);
+ }
+ });
+
+ removeElements.forEach((el) => {
+ el.parentElement.removeChild(el);
+ });
+
this.clearSearchButton.classList.add('hidden');
+ this.handleInputPlaceholder();
this.dropdownManager.resetDropdowns();
}
+ handleInputVisualToken() {
+ const input = this.filteredSearchInput;
+ const { tokens, searchToken }
+ = gl.FilteredSearchTokenizer.processTokens(input.value);
+ const { isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (isLastVisualTokenValid) {
+ tokens.forEach((t) => {
+ input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
+ });
+
+ 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);
+ 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;
+
+ if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
+
+ // Trim the last space as seen in the if statement above
+ input.value = input.value.replace(searchToken, '').trim();
+ }
+ }
+ }
+
handleFormSubmit(e) {
e.preventDefault();
this.search();
@@ -111,7 +249,7 @@
loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams();
- const inputValues = [];
+ let hasFilteredSearch = false;
params.forEach((p) => {
const split = p.split('=');
@@ -122,7 +260,8 @@
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) {
- inputValues.push(`${condition.tokenKey}:${condition.value}`);
+ 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 +
@@ -140,34 +279,37 @@
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
}
- inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
- inputValues.push(`assignee:@${usernameParams[id]}`);
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
- inputValues.push(`author:@${usernameParams[id]}`);
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'search') {
- inputValues.push(sanitizedValue);
+ hasFilteredSearch = true;
+ this.filteredSearchInput.value = sanitizedValue;
}
}
});
- // Trim the last space value
- this.filteredSearchInput.value = inputValues.join(' ');
-
- if (inputValues.length > 0) {
+ if (hasFilteredSearch) {
this.clearSearchButton.classList.remove('hidden');
+ this.handleInputPlaceholder();
}
}
search() {
const paths = [];
- const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
+ const { tokens, searchToken }
+ = this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery());
const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
new file mode 100644
index 00000000000..78b245726ee
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -0,0 +1,200 @@
+class FilteredSearchVisualTokens {
+ static getLastVisualTokenBeforeInput() {
+ const inputLi = document.querySelector('.input-token');
+ const lastVisualToken = inputLi && inputLi.previousElementSibling;
+
+ return {
+ lastVisualToken,
+ isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
+ };
+ }
+
+ static unselectTokens() {
+ const otherTokens = document.querySelectorAll('.js-visual-token .selectable.selected');
+ [].forEach.call(otherTokens, t => t.classList.remove('selected'));
+ }
+
+ static selectToken(tokenButton) {
+ const selected = tokenButton.classList.contains('selected');
+ FilteredSearchVisualTokens.unselectTokens();
+
+ if (!selected) {
+ tokenButton.classList.add('selected');
+ }
+ }
+
+ static removeSelectedToken() {
+ const selected = document.querySelector('.js-visual-token .selected');
+
+ if (selected) {
+ const li = selected.closest('.js-visual-token');
+ li.parentElement.removeChild(li);
+ }
+ }
+
+ static createVisualTokenElementHTML() {
+ return `
+ <div class="selectable" role="button">
+ <div class="name"></div>
+ <div class="value"></div>
+ </div>
+ `;
+ }
+
+ static addVisualTokenElement(name, value, isSearchTerm) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token');
+ li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
+
+ if (value) {
+ li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+ li.querySelector('.value').innerText = value;
+ } else {
+ li.innerHTML = '<div class="name"></div>';
+ }
+ li.querySelector('.name').innerText = name;
+
+ const tokensContainer = document.querySelector('.tokens-container');
+ const input = document.querySelector('.filtered-search');
+ tokensContainer.insertBefore(li, input.parentElement);
+ }
+
+ static addValueToPreviousVisualTokenElement(value) {
+ const { lastVisualToken, isLastVisualTokenValid } =
+ FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
+ const name = FilteredSearchVisualTokens.getLastTokenPartial();
+ lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+ lastVisualToken.querySelector('.name').innerText = name;
+ lastVisualToken.querySelector('.value').innerText = value;
+ }
+ }
+
+ static addFilterVisualToken(tokenName, tokenValue) {
+ const { lastVisualToken, isLastVisualTokenValid }
+ = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
+
+ if (isLastVisualTokenValid) {
+ addVisualTokenElement(tokenName, tokenValue);
+ } else {
+ const previousTokenName = lastVisualToken.querySelector('.name').innerText;
+ const tokensContainer = document.querySelector('.tokens-container');
+ tokensContainer.removeChild(lastVisualToken);
+
+ const value = tokenValue || tokenName;
+ addVisualTokenElement(previousTokenName, value);
+ }
+ }
+
+ static addSearchVisualToken(searchTerm) {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
+ lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
+ } else {
+ FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
+ }
+ }
+
+ static getLastTokenPartial() {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!lastVisualToken) return '';
+
+ const value = lastVisualToken.querySelector('.value');
+ const name = lastVisualToken.querySelector('.name');
+
+ const valueText = value ? value.innerText : '';
+ const nameText = name ? name.innerText : '';
+
+ return valueText || nameText;
+ }
+
+ static removeLastTokenPartial() {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (lastVisualToken) {
+ const value = lastVisualToken.querySelector('.value');
+
+ if (value) {
+ const button = lastVisualToken.querySelector('.selectable');
+ button.removeChild(value);
+ lastVisualToken.innerHTML = button.innerHTML;
+ } else {
+ lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
+ }
+ }
+ }
+
+ static tokenizeInput() {
+ const input = document.querySelector('.filtered-search');
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (input.value) {
+ if (isLastVisualTokenValid) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
+ } else {
+ FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
+ }
+
+ input.value = '';
+ }
+ }
+
+ static editToken(token) {
+ const input = document.querySelector('.filtered-search');
+
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ // Replace token with input field
+ const tokenContainer = token.parentElement;
+ const inputLi = input.parentElement;
+ tokenContainer.replaceChild(inputLi, token);
+
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+
+ if (token.classList.contains('filtered-search-token')) {
+ FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
+ input.value = value.innerText;
+ } else {
+ // token is a search term
+ input.value = name.innerText;
+ }
+
+ // Opens dropdown
+ const inputEvent = new Event('input');
+ input.dispatchEvent(inputEvent);
+
+ // Adds cursor to input
+ input.focus();
+ }
+
+ static moveInputToTheRight() {
+ const input = document.querySelector('.filtered-search');
+ const inputLi = input.parentElement;
+ const tokenContainer = document.querySelector('.tokens-container');
+
+ if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid) {
+ const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
+ }
+
+ tokenContainer.removeChild(inputLi);
+ tokenContainer.appendChild(inputLi);
+ }
+ }
+}
+
+window.gl = window.gl || {};
+gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens;