Add button to delete filters from filtered search bar
This commit is contained in:
parent
e2be17b78e
commit
09f09139e4
8 changed files with 145 additions and 21 deletions
|
@ -77,13 +77,14 @@ class FilteredSearchManager {
|
||||||
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
||||||
this.onClearSearchWrapper = this.onClearSearch.bind(this);
|
this.onClearSearchWrapper = this.onClearSearch.bind(this);
|
||||||
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
|
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
|
||||||
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
|
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
|
||||||
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
|
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
|
||||||
this.editTokenWrapper = this.editToken.bind(this);
|
this.editTokenWrapper = this.editToken.bind(this);
|
||||||
this.tokenChange = this.tokenChange.bind(this);
|
this.tokenChange = this.tokenChange.bind(this);
|
||||||
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
||||||
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
||||||
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
||||||
|
this.removeTokenWrapper = this.removeToken.bind(this);
|
||||||
|
|
||||||
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
||||||
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
||||||
|
@ -96,12 +97,13 @@ class FilteredSearchManager {
|
||||||
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
||||||
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
||||||
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
|
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
|
||||||
|
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
|
||||||
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
|
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
|
||||||
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
||||||
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||||
document.addEventListener('click', this.unselectEditTokensWrapper);
|
document.addEventListener('click', this.unselectEditTokensWrapper);
|
||||||
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
||||||
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
|
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
||||||
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,12 +119,13 @@ class FilteredSearchManager {
|
||||||
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
||||||
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
||||||
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
|
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
|
||||||
|
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
|
||||||
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
|
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
|
||||||
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
||||||
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||||
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
||||||
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
||||||
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
|
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
||||||
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,14 +198,28 @@ class FilteredSearchManager {
|
||||||
|
|
||||||
static selectToken(e) {
|
static selectToken(e) {
|
||||||
const button = e.target.closest('.selectable');
|
const button = e.target.closest('.selectable');
|
||||||
|
const removeButtonSelected = e.target.closest('.remove-token');
|
||||||
|
|
||||||
if (button) {
|
if (!removeButtonSelected && button) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
gl.FilteredSearchVisualTokens.selectToken(button);
|
gl.FilteredSearchVisualTokens.selectToken(button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeToken(e) {
|
||||||
|
const removeButtonSelected = e.target.closest('.remove-token');
|
||||||
|
|
||||||
|
if (removeButtonSelected) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const button = e.target.closest('.selectable');
|
||||||
|
gl.FilteredSearchVisualTokens.selectToken(button, true);
|
||||||
|
this.removeSelectedToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
@ -248,16 +265,21 @@ class FilteredSearchManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSelectedToken(e) {
|
removeSelectedTokenKeydown(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();
|
this.removeSelectedToken();
|
||||||
this.handleInputPlaceholder();
|
|
||||||
this.toggleClearSearchButton();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeSelectedToken() {
|
||||||
|
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
||||||
|
this.handleInputPlaceholder();
|
||||||
|
this.toggleClearSearchButton();
|
||||||
|
this.dropdownManager.updateCurrentDropdownOffset();
|
||||||
|
}
|
||||||
|
|
||||||
onClearSearch(e) {
|
onClearSearch(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.clearSearch();
|
this.clearSearch();
|
||||||
|
|
|
@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
|
||||||
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
|
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
|
||||||
}
|
}
|
||||||
|
|
||||||
static selectToken(tokenButton) {
|
static selectToken(tokenButton, forceSelection = false) {
|
||||||
const selected = tokenButton.classList.contains('selected');
|
const selected = tokenButton.classList.contains('selected');
|
||||||
FilteredSearchVisualTokens.unselectTokens();
|
FilteredSearchVisualTokens.unselectTokens();
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected || forceSelection) {
|
||||||
tokenButton.classList.add('selected');
|
tokenButton.classList.add('selected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
|
||||||
return `
|
return `
|
||||||
<div class="selectable" role="button">
|
<div class="selectable" role="button">
|
||||||
<div class="name"></div>
|
<div class="name"></div>
|
||||||
<div class="value"></div>
|
<div class="value-container">
|
||||||
|
<div class="value"></div>
|
||||||
|
<div class="remove-token" role="button">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const button = lastVisualToken.querySelector('.selectable');
|
const button = lastVisualToken.querySelector('.selectable');
|
||||||
button.removeChild(value);
|
const valueContainer = lastVisualToken.querySelector('.value-container');
|
||||||
|
button.removeChild(valueContainer);
|
||||||
lastVisualToken.innerHTML = button.innerHTML;
|
lastVisualToken.innerHTML = button.innerHTML;
|
||||||
} else {
|
} else {
|
||||||
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
|
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
|
||||||
|
|
|
@ -104,6 +104,24 @@
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-token {
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 8px;
|
||||||
|
|
||||||
|
.fa-close {
|
||||||
|
color: $gl-text-color-disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .fa-close {
|
||||||
|
color: $gl-text-color-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
background-color: $filter-name-resting-color;
|
background-color: $filter-name-resting-color;
|
||||||
color: $filter-name-text-color;
|
color: $filter-name-text-color;
|
||||||
|
@ -112,7 +130,7 @@
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value-container {
|
||||||
background-color: $white-normal;
|
background-color: $white-normal;
|
||||||
color: $filter-value-text-color;
|
color: $filter-value-text-color;
|
||||||
border-radius: 0 2px 2px 0;
|
border-radius: 0 2px 2px 0;
|
||||||
|
@ -124,7 +142,7 @@
|
||||||
background-color: $filter-name-selected-color;
|
background-color: $filter-name-selected-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value-container {
|
||||||
background-color: $filter-value-selected-color;
|
background-color: $filter-value-selected-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
4
changelogs/unreleased/30466-click-x-to-remove-filter.yml
Normal file
4
changelogs/unreleased/30466-click-x-to-remove-filter.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Add button to delete filters from filtered search bar
|
||||||
|
merge_request:
|
||||||
|
author:
|
|
@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do
|
||||||
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
|
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
|
||||||
|
|
||||||
let!(:bug_label) { create(:label, project: project, title: 'bug') }
|
let!(:bug_label) { create(:label, project: project, title: 'bug') }
|
||||||
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
|
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
|
||||||
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
|
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
|
||||||
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
|
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,10 @@ describe('Filtered Search Manager', () => {
|
||||||
element.dispatchEvent(event);
|
element.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVisualTokens() {
|
||||||
|
return tokensContainer.querySelectorAll('.js-visual-token');
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setFixtures(`
|
setFixtures(`
|
||||||
<div class="filtered-search-box">
|
<div class="filtered-search-box">
|
||||||
|
@ -170,11 +174,37 @@ describe('Filtered Search Manager', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removeSelectedToken', () => {
|
describe('removeToken', () => {
|
||||||
function getVisualTokens() {
|
it('removes token even when it is already selected', () => {
|
||||||
return tokensContainer.querySelectorAll('.js-visual-token');
|
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
|
||||||
}
|
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
|
||||||
|
);
|
||||||
|
|
||||||
|
tokensContainer.querySelector('.js-visual-token .remove-token').click();
|
||||||
|
expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unselected token', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
|
||||||
|
|
||||||
|
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
|
||||||
|
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
|
||||||
|
);
|
||||||
|
tokensContainer.querySelector('.js-visual-token .remove-token').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes token when remove button is selected', () => {
|
||||||
|
expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls removeSelectedToken', () => {
|
||||||
|
expect(manager.removeSelectedToken).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSelectedTokenKeydown', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
|
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
|
||||||
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
|
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
|
||||||
|
@ -224,6 +254,31 @@ describe('Filtered Search Manager', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeSelectedToken', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
|
||||||
|
spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
|
||||||
|
spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
|
||||||
|
manager.removeSelectedToken();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
|
||||||
|
expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls handleInputPlaceholder', () => {
|
||||||
|
expect(manager.handleInputPlaceholder).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls toggleClearSearchButton', () => {
|
||||||
|
expect(manager.toggleClearSearchButton).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls update dropdown offset', () => {
|
||||||
|
expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('unselects token', () => {
|
describe('unselects token', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
|
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
|
||||||
|
|
|
@ -214,8 +214,12 @@ describe('Filtered Search Visual Tokens', () => {
|
||||||
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
|
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('contains value container div', () => {
|
||||||
|
expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything());
|
||||||
|
});
|
||||||
|
|
||||||
it('contains value div', () => {
|
it('contains value div', () => {
|
||||||
expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
|
expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains selectable class', () => {
|
it('contains selectable class', () => {
|
||||||
|
@ -225,6 +229,16 @@ describe('Filtered Search Visual Tokens', () => {
|
||||||
it('contains button role', () => {
|
it('contains button role', () => {
|
||||||
expect(tokenElement.getAttribute('role')).toEqual('button');
|
expect(tokenElement.getAttribute('role')).toEqual('button');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('remove token', () => {
|
||||||
|
it('contains remove-token button', () => {
|
||||||
|
expect(tokenElement.querySelector('.value-container .remove-token')).toEqual(jasmine.anything());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains fa-close icon', () => {
|
||||||
|
expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addVisualTokenElement', () => {
|
describe('addVisualTokenElement', () => {
|
||||||
|
|
|
@ -10,7 +10,12 @@ class FilteredSearchSpecHelper {
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
|
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
|
||||||
<div class="name">${name}</div>
|
<div class="name">${name}</div>
|
||||||
<div class="value">${value}</div>
|
<div class="value-container">
|
||||||
|
<div class="value">${value}</div>
|
||||||
|
<div class="remove-token" role="button">
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue