diff options
author | Jacob Schatz <jschatz@gitlab.com> | 2017-01-21 21:34:32 +0000 |
---|---|---|
committer | Stan Hu <stanhu@gmail.com> | 2017-01-21 22:19:38 -0800 |
commit | 7346565e361a12fe4215e05fb24b8e6c1729760c (patch) | |
tree | a8cdba0b6178ceb56d690d9d4077e03db0727698 | |
parent | b16f8bea430b1e08cd28d026c283806f1997dafe (diff) | |
download | gitlab-ce-7346565e361a12fe4215e05fb24b8e6c1729760c.tar.gz |
Merge branch 'issue-search-token-position' into 'master'
Filtered search input click back at token
See merge request !8617
8 files changed, 165 insertions, 41 deletions
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 63c20f57520..f4ec3b206cc 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -9,7 +9,7 @@ this.config = { droplabFilter: { template: 'hint', - filterFunction: gl.DropdownUtils.filterHint, + filterFunction: gl.DropdownUtils.filterHint.bind(null, input), }, }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index f06c3fc9c6f..13cbec1be4a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -15,7 +15,7 @@ loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol), + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), }, }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index e80d266ae89..7bf199d9274 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -37,7 +37,7 @@ } getSearchInput() { - const query = this.input.value.trim(); + const query = gl.DropdownUtils.getSearchInput(this.input); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); return lastToken.value || ''; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index c27ef3042d1..443ac222f70 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -20,17 +20,15 @@ return escapedText; } - static filterWithSymbol(filterSymbol, item, query) { + static filterWithSymbol(filterSymbol, input, item) { const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); if (lastToken !== searchToken) { const title = updatedItem.title.toLowerCase(); let value = lastToken.value.toLowerCase(); - - if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { - value = value.slice(1); - } + value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -1)); // Eg. filterSymbol = ~ for labels const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; @@ -44,8 +42,9 @@ return updatedItem; } - static filterHint(item, query) { + static filterHint(input, item) { const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); lastToken = lastToken.key || lastToken || ''; @@ -72,6 +71,48 @@ // Return boolean based on whether it was set return dataValue !== null; } + + static getSearchInput(filteredSearchInput) { + const inputValue = filteredSearchInput.value; + const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + + return inputValue.slice(0, right); + } + + static getInputSelectionPosition(input) { + const selectionStart = input.selectionStart; + let inputValue = input.value; + // Replace all spaces inside quote marks with underscores + // This helps with matching the beginning & end of a token:key + inputValue = inputValue.replace(/"(.*?)"/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+$/); + + if (selectionStart === 0) { + left = 0; + } else if (selectionStart === inputValue.length && left < 0) { + left = inputValue.length; + } else if (left < 0) { + left = selectionStart; + } + + return { + left, + right, + }; + } } window.gl = window.gl || {}; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 1cd0483877a..04873115580 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -57,28 +57,25 @@ static addWordToInput(tokenName, tokenValue = '') { const input = document.querySelector('.filtered-search'); + const inputValue = input.value; const word = `${tokenName}:${tokenValue}`; - const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value); - const lastSearchToken = searchToken.split(' ').last(); - const lastInputCharacter = input.value[input.value.length - 1]; - const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1]; - - // Remove the typed tokenName - if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') { - // Remove spaces after the colon - if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') { - input.value = input.value.trim(); - } - - input.value = input.value.slice(0, -1 * lastSearchToken.length); - } else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) { - // Remove the existing tokenValue - const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`; - input.value = input.value.slice(0, -1 * lastTokenString.length); - } + // Get the string to replace + const selectionStart = input.selectionStart; + const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; + gl.FilteredSearchDropdownManager.updateInputCaretPosition(selectionStart, 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.value += word; + input.setSelectionRange(right, right); } updateCurrentDropdownOffset() { @@ -90,9 +87,10 @@ this.font = window.getComputedStyle(this.filteredSearchInput).font; } + const input = this.filteredSearchInput; + const inputText = input.value.slice(0, input.selectionStart); const filterIconPadding = 27; - const offset = gl.text - .getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + const offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; this.mapping[key].reference.setOffset(offset); } @@ -148,9 +146,9 @@ setDropdown() { const { lastToken, searchToken } = this.tokenizer - .processTokens(this.filteredSearchInput.value); + .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); - if (this.filteredSearchInput.value.split('').last() === ' ') { + if (this.currentDropdown) { this.updateCurrentDropdownOffset(); } 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 4e8a7cfc940..c7b72b36561 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -30,11 +30,14 @@ this.checkForEnterWrapper = this.checkForEnter.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.tokenChange = this.tokenChange.bind(this); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); 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.clearSearchButton.addEventListener('click', this.clearSearchWrapper); } @@ -43,6 +46,8 @@ this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); 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.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); } @@ -188,6 +193,14 @@ } return usernamesById; } + + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } } window.gl = window.gl || {}; diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index ead43d6784a..1cdac520181 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -19,9 +19,12 @@ describe 'Filter issues', js: true, feature: true do let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } let(:filtered_search) { find('.filtered-search') } - def input_filtered_search(search_term) + def input_filtered_search(search_term, submit: true) filtered_search.set(search_term) - filtered_search.send_keys(:enter) + + if submit + filtered_search.send_keys(:enter) + end end def expect_filtered_search_input(input) @@ -43,6 +46,10 @@ describe 'Filter issues', js: true, feature: true do end end + def select_search_at_index(pos) + evaluate_script("el = document.querySelector('.filtered-search'); el.focus(); el.setSelectionRange(#{pos}, #{pos});") + end + before do project.team << [user, :master] project.team << [user2, :master] @@ -522,6 +529,44 @@ describe 'Filter issues', js: true, feature: true do end end + describe 'overwrites selected filter' do + it 'changes author' do + input_filtered_search("author:@#{user.username}", submit: false) + + select_search_at_index(3) + + page.within '#js-dropdown-author' do + click_button user2.username + end + + expect(filtered_search.value).to eq("author:@#{user2.username}") + end + + it 'changes label' do + input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false) + + select_search_at_index(27) + + page.within '#js-dropdown-label' do + click_button label.name + end + + expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name}") + end + + it 'changes label correctly space is in previous label' do + input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false) + + select_search_at_index(0) + + page.within '#js-dropdown-label' do + click_button label.name + end + + expect(filtered_search.value).to eq("label:~#{label.name}") + end + end + describe 'filter issues by text' do context 'only text' do it 'filters issues by searched text' do diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index ce61b73aa8a..19bd8d53219 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -31,41 +31,68 @@ }); describe('filterWithSymbol', () => { + let input; const item = { title: '@root', }; + beforeEach(() => { + setFixtures(` + <input type="text" id="test" /> + `); + + input = document.getElementById('test'); + }); + it('should filter without symbol', () => { - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo'); + input.value = ':roo'; + + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with symbol', () => { - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo'); + input.value = '@roo'; + + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with colon', () => { - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':'); + input.value = 'roo'; + + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); }); }); describe('filterHint', () => { + let input; + + beforeEach(() => { + setFixtures(` + <input type="text" id="test" /> + `); + + input = document.getElementById('test'); + }); + it('should filter', () => { - let updatedItem = gl.DropdownUtils.filterHint({ + input.value = 'l'; + let updatedItem = gl.DropdownUtils.filterHint(input, { hint: 'label', - }, 'l'); + }); expect(updatedItem.droplab_hidden).toBe(false); - updatedItem = gl.DropdownUtils.filterHint({ + input.value = 'o'; + updatedItem = gl.DropdownUtils.filterHint(input, { hint: 'label', }, 'o'); expect(updatedItem.droplab_hidden).toBe(true); }); it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterHint({}, ''); + const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); expect(updatedItem.droplab_hidden).toBe(false); }); }); |