summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClement Ho <ClemMakesApps@gmail.com>2017-02-24 10:09:53 -0600
committerClement Ho <ClemMakesApps@gmail.com>2017-03-01 09:29:04 -0600
commit6d10767fb6bb5197f20260203264739bbb0e0e4a (patch)
tree92ea302bdf4df6ac70cdc7612b0728f30d599f49
parent1f40c8a93d35cec4edd9e5208c861ab6c29fcf47 (diff)
downloadgitlab-ce-highlight-selected-tokens.tar.gz
Allow visual tokens to be visually selectablehighlight-selected-tokens
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es615
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es632
-rw-r--r--app/assets/stylesheets/framework/filters.scss19
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6139
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es662
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js.es68
7 files changed, 190 insertions, 87 deletions
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index 081c2fa9aeb..d216fff07e3 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -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) {
@@ -43,7 +44,9 @@
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.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
+ document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
}
unbindEvents() {
@@ -56,7 +59,9 @@
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.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
+ document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
}
checkForBackspace(e) {
@@ -99,6 +104,16 @@
}
}
+ static selectToken(e) {
+ const button = e.target.closest('.selectable');
+
+ if (button) {
+ e.preventDefault();
+ e.stopPropagation();
+ gl.FilteredSearchVisualTokens.selectToken(button);
+ }
+ }
+
toggleClearSearchButton() {
const query = gl.DropdownUtils.getSearchQuery();
const hidden = 'hidden';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6
index 33f9d2e2651..3b04c6ef6fa 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6
@@ -9,17 +9,37 @@ class FilteredSearchVisualTokens {
};
}
+ 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 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');
- li.innerHTML = '<div class="name"></div>';
- li.querySelector('.name').innerText = name;
if (value) {
- li.innerHTML += '<div class="value"></div>';
+ li.innerHTML = `
+ <div class="selectable" role="button">
+ <div class="name"></div>
+ <div class="value"></div>
+ </div>
+ `;
li.querySelector('.value').innerText = value;
+ } else {
+ li.innerHTML = '<div class="name"></div>';
}
+ li.querySelector('.name').innerText = name;
const tokensContainer = document.querySelector('.tokens-container');
tokensContainer.appendChild(li);
@@ -67,9 +87,11 @@ class FilteredSearchVisualTokens {
const value = lastVisualToken.querySelector('.value');
if (value) {
- lastVisualToken.removeChild(value);
+ const button = lastVisualToken.querySelector('.selectable');
+ button.removeChild(value);
+ lastVisualToken.innerHTML = button.innerHTML;
} else {
- lastVisualToken.parentElement.removeChild(lastVisualToken);
+ lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
}
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index fbb2e728b0a..64e3251a849 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -49,6 +49,11 @@
margin-top: 5px;
margin-bottom: 5px;
+ .selectable {
+ display: -webkit-flex;
+ display: flex;
+ }
+
.name,
.value {
display: inline-block;
@@ -69,6 +74,16 @@
border-radius: 0 2px 2px 0;
margin-right: 5px;
}
+
+ .selected {
+ .name {
+ background-color: $filter-name-selected-color;
+ }
+
+ .value {
+ background-color: $filter-value-selected-color;
+ }
+ }
}
.filtered-search-term {
@@ -77,6 +92,10 @@
color: $black;
text-transform: none;
}
+
+ .selectable {
+ cursor: text;
+ }
}
.scroll-container {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index cbf7270bf65..b9f1d781ea8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -547,3 +547,5 @@ Filtered Search
$filter-name-resting-color: #f8f8f8;
$filter-name-text-color: rgba(0, 0, 0, 0.55);
$filter-value-text-color: rgba(0, 0, 0, 0.85);
+$filter-name-selected-color: #ebebeb;
+$filter-value-selected-color: #d7d7d7;
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
index 975fb1da40a..51582251cbe 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
@@ -9,30 +9,40 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
(() => {
describe('Filtered Search Manager', () => {
let input;
+ let manager;
+ let tokensContainer;
+ const placeholder = 'Search or filter results...';
+
+ beforeEach(() => {
+ setFixtures(`
+ <form>
+ <ul class="tokens-container list-unstyled"></ul>
+ <input type='text' class='filtered-search' placeholder='${placeholder}' />
+ <button class="clear-search" type="button">
+ <i class="fa fa-times"></i>
+ </button>
+ </form>
+ `);
+
+ spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
+ 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);
+
+ input = document.querySelector('.filtered-search');
+ tokensContainer = document.querySelector('.tokens-container');
+ manager = new gl.FilteredSearchManager();
+ });
+
+ afterEach(() => {
+ tokensContainer.innerHTML = '';
+ });
describe('search', () => {
- let manager;
const defaultParams = '?scope=all&utf8=✓&state=opened';
- beforeEach(() => {
- setFixtures(`
- <input type='text' class='filtered-search' />
- `);
-
- spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
- spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
- spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
-
- input = document.querySelector('.filtered-search');
- manager = new gl.FilteredSearchManager();
- });
-
- afterEach(() => {
- input.outerHTML = '';
- });
-
it('should search with a single word', () => {
input.value = 'searchTerm';
@@ -65,35 +75,6 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
describe('handleInputPlaceholder', () => {
- const placeholder = 'Search or filter results...';
- let tokensContainer;
-
- beforeEach(() => {
- setFixtures(`
- <form>
- <ul class="tokens-container list-unstyled"></ul>
- <input type='text' class='filtered-search' placeholder='${placeholder}' />
- <button class="clear-search" type="button">
- <i class="fa fa-times"></i>
- </button>
- </form>
- `);
-
- spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
- spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
- spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
-
- input = document.querySelector('.filtered-search');
- tokensContainer = document.querySelector('.tokens-container');
- return new gl.FilteredSearchManager();
- });
-
- afterEach(() => {
- input.outerHTML = '';
- tokensContainer.innerHTML = '';
- });
-
it('should render placeholder when there is no input', () => {
expect(input.placeholder).toEqual(placeholder);
});
@@ -118,39 +99,9 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
describe('checkForBackspace', () => {
- let tokensContainer;
const backspaceKey = 8;
const deleteKey = 46;
- beforeEach(() => {
- setFixtures(`
- <form>
- <ul class="tokens-container list-unstyled"></ul>
- <input type='text' class='filtered-search' />
- <button class="clear-search" type="button">
- <i class="fa fa-times"></i>
- </button>
- </form>
- `);
-
- spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
- 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);
-
- input = document.querySelector('.filtered-search');
- tokensContainer = document.querySelector('.tokens-container');
- return new gl.FilteredSearchManager();
- });
-
- afterEach(() => {
- input.outerHTML = '';
- tokensContainer.innerHTML = '';
- });
-
describe('tokens and no input', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug');
@@ -193,5 +144,35 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
expect(input.value).toEqual('text');
});
});
+
+ describe('unselects token', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `;
+ });
+
+ it('unselects token when input is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+
+ input.click();
+
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ });
+
+ it('unselects token when document.body is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+
+ document.body.click();
+
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ });
+ });
});
})();
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6
index 8c5150a6134..ff21d096f9d 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6
@@ -76,6 +76,68 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
});
+ describe('unselectTokens', () => {
+ it('does nothing when there are no tokens', () => {
+ const beforeHTML = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.unselectTokens();
+
+ expect(tokensContainer.innerHTML).toEqual(beforeHTML);
+ });
+
+ it('removes the selected class from buttons', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)}
+ `;
+
+ const selected = tokensContainer.querySelector('.js-visual-token .selected');
+ expect(selected.classList.contains('selected')).toEqual(true);
+
+ gl.FilteredSearchVisualTokens.unselectTokens();
+
+ expect(selected.classList.contains('selected')).toEqual(false);
+ });
+ });
+
+ describe('selectToken', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `;
+ });
+
+ it('removes the selected class if it has selected class', () => {
+ const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+ firstTokenButton.classList.add('selected');
+
+ gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+ expect(firstTokenButton.classList.contains('selected')).toEqual(false);
+ });
+
+ describe('has no selected class', () => {
+ it('adds selected class', () => {
+ const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+
+ gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+ expect(firstTokenButton.classList.contains('selected')).toEqual(true);
+ });
+
+ it('removes selected class from other tokens', () => {
+ const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
+ tokenButtons[1].classList.add('selected');
+
+ gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]);
+
+ expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
+ expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
+ });
+ });
+ });
+
describe('addVisualTokenElement', () => {
it('renders search visual tokens', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js.es6 b/spec/javascripts/helpers/filtered_search_spec_helper.js.es6
index 567efdd838c..31f4d93f8b8 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js.es6
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js.es6
@@ -1,9 +1,11 @@
class FilteredSearchSpecHelper {
- static createFilterVisualTokenHTML(name, value) {
+ static createFilterVisualTokenHTML(name, value, isSelected = false) {
return `
<li class="js-visual-token filtered-search-token">
- <div class="name">${name}</div>
- <div class="value">${value}</div>
+ <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
+ <div class="name">${name}</div>
+ <div class="value">${value}</div>
+ </div>
</li>
`;
}