diff options
Diffstat (limited to 'app/assets/javascripts')
13 files changed, 228 insertions, 82 deletions
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index ed545ec8748..c79f0230951 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -62,6 +62,7 @@ var DropDown = function(list) { this.list = list; this.items = []; this.getItems(); + this.initTemplateString(); this.addEvents(); this.initialState = list.innerHTML; }; @@ -72,6 +73,17 @@ Object.assign(DropDown.prototype, { return this.items; }, + initTemplateString: function() { + var items = this.items || this.getItems(); + + var templateString = ''; + if(items.length > 0) { + templateString = items[items.length - 1].outerHTML; + } + this.templateString = templateString; + return this.templateString; + }, + clickEvent: function(e) { // climb up the tree to find the LI var selected = utils.closest(e.target, 'LI'); @@ -111,30 +123,21 @@ Object.assign(DropDown.prototype, { addData: function(data) { this.data = (this.data || []).concat(data); - this.render(data); + this.render(this.data); }, // call render manually on data; render: function(data){ // debugger // empty the list first - var sampleItem; + var templateString = this.templateString; var newChildren = []; var toAppend; - for(var i = 0; i < this.items.length; i++) { - var item = this.items[i]; - sampleItem = item; - if(item.parentNode && item.parentNode.dataset.hasOwnProperty('dynamic')) { - item.parentNode.removeChild(item); - } - } - - newChildren = this.data.map(function(dat){ - var html = utils.t(sampleItem.outerHTML, dat); + newChildren = (data ||[]).map(function(dat){ + var html = utils.t(templateString, dat); var template = document.createElement('div'); template.innerHTML = html; - // console.log(template.content) // Help set the image src template var imageTags = template.querySelectorAll('img[data-src]'); @@ -156,7 +159,7 @@ Object.assign(DropDown.prototype, { if(toAppend) { toAppend.innerHTML = newChildren.join(''); } else { - this.list.innerHTML = newChildren.join(''); + this.list.innerHTML = newChildren.join(''); } }, @@ -173,10 +176,7 @@ Object.assign(DropDown.prototype, { }, destroy: function() { - if (!this.hidden) { - this.hide(); - } - + this.hide(); this.list.removeEventListener('click', this.clickWrapper); } }); @@ -278,7 +278,7 @@ require('./window')(function(w){ self.hooks[i].list.hide(); } }.bind(this); - w.addEventListener('click', this.windowClickedWrapper); + document.addEventListener('click', this.windowClickedWrapper); }, removeEvents: function(){ @@ -307,7 +307,7 @@ require('./window')(function(w){ if(!list){ list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); } - + if(hook) { if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { this.hooks.push(new HookButton(hook, list, plugins, config)); @@ -462,6 +462,8 @@ Object.assign(HookInput.prototype, { var self = this; this.mousedown = function mousedown(e) { + if(self.hasRemovedEvents) return; + var mouseEvent = new CustomEvent('mousedown.dl', { detail: { hook: self, @@ -474,6 +476,8 @@ Object.assign(HookInput.prototype, { } this.input = function input(e) { + if(self.hasRemovedEvents) return; + var inputEvent = new CustomEvent('input.dl', { detail: { hook: self, @@ -487,10 +491,14 @@ Object.assign(HookInput.prototype, { } this.keyup = function keyup(e) { + if(self.hasRemovedEvents) return; + keyEvent(e, 'keyup.dl'); } this.keydown = function keydown(e) { + if(self.hasRemovedEvents) return; + keyEvent(e, 'keydown.dl'); } @@ -520,7 +528,8 @@ Object.assign(HookInput.prototype, { this.trigger.addEventListener('keydown', this.keydown); }, - removeEvents: function(){ + removeEvents: function() { + this.hasRemovedEvents = true; this.trigger.removeEventListener('mousedown', this.mousedown); this.trigger.removeEventListener('input', this.input); this.trigger.removeEventListener('keyup', this.keyup); @@ -578,7 +587,7 @@ require('./window')(function(w){ var listItems = removeHighlight(list); if(currentIndex>0){ if(!listItems[currentIndex-1]){ - currentIndex = currentIndex-1; + currentIndex = currentIndex-1; } listItems[currentIndex-1].classList.add('dropdown-active'); } @@ -630,7 +639,7 @@ require('./window')(function(w){ return; } if(currentKey === 'ArrowUp') { - isUpArrow = true; + isUpArrow = true; } if(currentKey === 'ArrowDown') { isDownArrow = true; @@ -668,16 +677,16 @@ var camelize = function(str) { }; var closest = function(thisTag, stopTag) { - while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ thisTag = thisTag.parentNode; } return thisTag; }; var isDropDownParts = function(target) { - if(target.tagName === 'HTML') { return false; } + if(!target || target.tagName === 'HTML') { return false; } return ( - target.hasAttribute(DATA_TRIGGER) || + target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN) ); }; diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6 new file mode 100644 index 00000000000..abedae4c1c7 --- /dev/null +++ b/app/assets/javascripts/extensions/custom_event.js.es6 @@ -0,0 +1,12 @@ +/* global CustomEvent */ +/* eslint-disable no-global-assign */ + +// Custom event support for IE +CustomEvent = function CustomEvent(event, parameters) { + const params = parameters || { bubbles: false, cancelable: false, detail: undefined }; + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; +}; + +CustomEvent.prototype = window.Event.prototype; 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.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index 886d8113f4a..9128ea907b3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -78,7 +78,10 @@ dispatchInputEvent() { // Propogate input change to FilteredSearchDropdownManager // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); + this.input.dispatchEvent(new CustomEvent('input', { + bubbles: true, + cancelable: true, + })); } hideDropdown() { 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..408a0dfd768 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); - input.value += word; + const { right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.setSelectionRange(right, right); } updateCurrentDropdownOffset() { @@ -90,9 +87,18 @@ 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; + let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; + + const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : + this.mapping[key].element.clientWidth; + const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; + + if (offsetMaxWidth < offset) { + offset = offsetMaxWidth; + } this.mapping[key].reference.setOffset(offset); } @@ -148,9 +154,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 ffd0d7e9cba..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); } @@ -85,6 +90,7 @@ loadSearchParamsFromURL() { const params = gl.utils.getUrlParamsArray(); + const usernameParams = this.getUsernameParams(); const inputValues = []; params.forEach((p) => { @@ -115,6 +121,16 @@ } inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'assignee_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`assignee:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'author_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`author:@${usernameParams[id]}`); + } } else if (!match && keyParam === 'search') { inputValues.push(sanitizedValue); } @@ -164,6 +180,27 @@ Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); } + + getUsernameParams() { + 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; + } + + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } } window.gl = window.gl || {}; diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index 5247b2a08f7..10dfd05fe3c 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -2,12 +2,12 @@ (function() { this.GroupAvatar = (function() { function GroupAvatar() { - $('.js-choose-group-avatar-button').bind("click", function() { + $('.js-choose-group-avatar-button').on("click", function() { var form; form = $(this).closest("form"); return form.find(".js-group-avatar-input").click(); }); - $('.js-group-avatar-input').bind("change", function() { + $('.js-group-avatar-input').on("change", function() { var filename, form; form = $(this).closest("form"); filename = $(this).val().replace(/^.*[\\\/]/, ''); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7a315e43667..7cc319e2f4e 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -126,7 +126,9 @@ MergeRequestWidget.prototype.getMergeStatus = function() { return $.get(this.opts.merge_check_url, function(data) { - return $('.mr-state-widget').replaceWith(data); + var $html = $(data); + $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); + $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); }); }; diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 index 2b074994b4a..5969d2ba56b 100644 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -8,31 +8,42 @@ * temporarily. * */ - if ($('.accept-mr-form').length) { - $('.accept-mr-form').on('ajax:send', () => { - $('.accept-mr-form :input').disable(); - }); + $(document) + .off('ajax:send', '.accept-mr-form') + .on('ajax:send', '.accept-mr-form', () => { + $('.accept-mr-form :input').disable(); + }); - $('.accept_merge_request').on('click', () => { - $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); - }); + $(document) + .off('click', '.accept_merge_request') + .on('click', '.accept_merge_request', () => { + $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); + }); - $('.merge_when_build_succeeds').on('click', () => { - $('#merge_when_build_succeeds').val('1'); - }); + $(document) + .off('click', '.merge_when_build_succeeds') + .on('click', '.merge_when_build_succeeds', () => { + $('#merge_when_build_succeeds').val('1'); + }); - $('.js-merge-dropdown a').on('click', (e) => { - e.preventDefault(); - $(this).closest('form').submit(); - }); - } else if ($('.rebase-in-progress').length) { + $(document) + .off('click', '.js-merge-dropdown a') + .on('click', '.js-merge-dropdown a', (e) => { + e.preventDefault(); + $(e.target).closest('form').submit(); + }); + if ($('.rebase-in-progress').length) { merge_request_widget.rebaseInProgress(); } else if ($('.rebase-mr-form').length) { - $('.rebase-mr-form').on('ajax:send', () => { + $(document) + .off('ajax:send', '.rebase-mr-form') + .on('ajax:send', '.rebase-mr-form', () => { $('.rebase-mr-form :input').disable(); }); - $('.js-rebase-button').on('click', () => { + $(document) + .off('click', '.js-rebase-button') + .on('click', '.js-rebase-button', () => { $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); }); } else { diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 32973132174..4e85f16ebc5 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -1,5 +1,5 @@ /* global Vue, Flash, gl */ -/* eslint-disable no-param-reassign, no-bitwise */ +/* eslint-disable no-param-reassign */ ((gl) => { gl.VueStage = Vue.extend({ @@ -9,7 +9,20 @@ spinner: '<span class="fa fa-spinner fa-spin"></span>', }; }, - props: ['stage', 'svgs', 'match'], + props: { + stage: { + type: Object, + required: true, + }, + svgs: { + type: DOMStringMap, + required: true, + }, + match: { + type: Function, + required: true, + }, + }, methods: { fetchBuilds(e) { const areaExpanded = e.currentTarget.attributes['aria-expanded']; @@ -24,6 +37,18 @@ return flash; }); }, + keepGraph(e) { + const { target } = e; + + if (target.className.indexOf('js-ci-action-icon') >= 0) return null; + + if ( + target.parentElement && + (target.parentElement.className.indexOf('js-ci-action-icon') >= 0) + ) return null; + + return e.stopPropagation(); + }, }, computed: { buildsOrSpinner() { @@ -64,7 +89,7 @@ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <div class="arrow-up"></div> <div - @click='' + @click='keepGraph($event)' :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner" |