Commit c8c38f94 authored by Phil Hughes's avatar Phil Hughes

Merge branch '36409-frontend-for-clarifying-the-usefulness-of-the-search-bar' into 'master'

Resolve "Frontend for clarifying the usefulness of the search bar"

Closes #36409

See merge request gitlab-org/gitlab-ce!20537
parents 8a1d55a3 efec7e08
/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */ /* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, max-len, vars-on-top, wrap-iife, no-unused-vars, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
/* global fuzzaldrinPlus */ /* global fuzzaldrinPlus */
import $ from 'jquery'; import $ from 'jquery';
...@@ -19,32 +19,42 @@ GitLabDropdownInput = (function() { ...@@ -19,32 +19,42 @@ GitLabDropdownInput = (function() {
this.fieldName = this.options.fieldName || 'field-name'; this.fieldName = this.options.fieldName || 'field-name';
$inputContainer = this.input.parent(); $inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear'); $clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) { $clearButton.on(
// Clear click 'click',
return function(e) { (function(_this) {
e.preventDefault(); // Clear click
e.stopPropagation(); return function(e) {
return _this.input.val('').trigger('input').focus(); e.preventDefault();
}; e.stopPropagation();
})(this)); return _this.input
.val('')
.trigger('input')
.focus();
};
})(this),
);
this.input this.input
.on('keydown', function (e) { .on('keydown', function(e) {
var keyCode = e.which; var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) { if (keyCode === 13 && !options.elIsInput) {
e.preventDefault(); e.preventDefault();
} }
}) })
.on('input', function(e) { .on('input', function(e) {
var val = e.currentTarget.value || _this.options.inputFieldName; var val = e.currentTarget.value || _this.options.inputFieldName;
val = val.split(' ').join('-') // replaces space with dash val = val
.replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric .split(' ')
.replace(/(-)\1+/g, '-'); // replace repeated dashes .join('-') // replaces space with dash
_this.cb(_this.options.fieldName, val, {}, true); .replace(/[^a-zA-Z0-9 -]/g, '')
_this.input.closest('.dropdown') .toLowerCase() // replace non alphanumeric
.find('.dropdown-toggle-text') .replace(/(-)\1+/g, '-'); // replace repeated dashes
.text(val); _this.cb(_this.options.fieldName, val, {}, true);
}); _this.input
.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
});
} }
GitLabDropdownInput.prototype.onInput = function(cb) { GitLabDropdownInput.prototype.onInput = function(cb) {
...@@ -61,7 +71,7 @@ GitLabDropdownFilter = (function() { ...@@ -61,7 +71,7 @@ GitLabDropdownFilter = (function() {
ARROW_KEY_CODES = [38, 40]; ARROW_KEY_CODES = [38, 40];
HAS_VALUE_CLASS = "has-value"; HAS_VALUE_CLASS = 'has-value';
function GitLabDropdownFilter(input, options) { function GitLabDropdownFilter(input, options) {
var $clearButton, $inputContainer, ref, timeout; var $clearButton, $inputContainer, ref, timeout;
...@@ -70,44 +80,59 @@ GitLabDropdownFilter = (function() { ...@@ -70,44 +80,59 @@ GitLabDropdownFilter = (function() {
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent(); $inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear'); $clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) { $clearButton.on(
// Clear click 'click',
return function(e) { (function(_this) {
e.preventDefault(); // Clear click
e.stopPropagation(); return function(e) {
return _this.input.val('').trigger('input').focus(); e.preventDefault();
}; e.stopPropagation();
})(this)); return _this.input
.val('')
.trigger('input')
.focus();
};
})(this),
);
// Key events // Key events
timeout = ""; timeout = '';
this.input this.input
.on('keydown', function (e) { .on('keydown', function(e) {
var keyCode = e.which; var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) { if (keyCode === 13 && !options.elIsInput) {
e.preventDefault(); e.preventDefault();
} }
}) })
.on('input', function() { .on(
if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { 'input',
$inputContainer.addClass(HAS_VALUE_CLASS); function() {
} else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) { if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS); $inputContainer.addClass(HAS_VALUE_CLASS);
} } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
// Only filter asynchronously only if option remote is set $inputContainer.removeClass(HAS_VALUE_CLASS);
if (this.options.remote) { }
clearTimeout(timeout); // Only filter asynchronously only if option remote is set
return timeout = setTimeout(function() { if (this.options.remote) {
$inputContainer.parent().addClass('is-loading'); clearTimeout(timeout);
return (timeout = setTimeout(
return this.options.query(this.input.val(), function(data) { function() {
$inputContainer.parent().removeClass('is-loading'); $inputContainer.parent().addClass('is-loading');
return this.options.callback(data);
}.bind(this)); return this.options.query(
}.bind(this), 250); this.input.val(),
} else { function(data) {
return this.filter(this.input.val()); $inputContainer.parent().removeClass('is-loading');
} return this.options.callback(data);
}.bind(this)); }.bind(this),
);
}.bind(this),
250,
));
} else {
return this.filter(this.input.val());
}
}.bind(this),
);
} }
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
...@@ -120,7 +145,7 @@ GitLabDropdownFilter = (function() { ...@@ -120,7 +145,7 @@ GitLabDropdownFilter = (function() {
this.options.onFilter(search_text); this.options.onFilter(search_text);
} }
data = this.options.data(); data = this.options.data();
if ((data != null) && !this.options.filterByText) { if (data != null && !this.options.filterByText) {
results = data; results = data;
if (search_text !== '') { if (search_text !== '') {
// When data is an array of objects therefore [object Array] e.g. // When data is an array of objects therefore [object Array] e.g.
...@@ -130,7 +155,7 @@ GitLabDropdownFilter = (function() { ...@@ -130,7 +155,7 @@ GitLabDropdownFilter = (function() {
// ] // ]
if (_.isArray(data)) { if (_.isArray(data)) {
results = fuzzaldrinPlus.filter(data, search_text, { results = fuzzaldrinPlus.filter(data, search_text, {
key: this.options.keys key: this.options.keys,
}); });
} else { } else {
// If data is grouped therefore an [object Object]. e.g. // If data is grouped therefore an [object Object]. e.g.
...@@ -149,7 +174,7 @@ GitLabDropdownFilter = (function() { ...@@ -149,7 +174,7 @@ GitLabDropdownFilter = (function() {
for (key in data) { for (key in data) {
group = data[key]; group = data[key];
tmp = fuzzaldrinPlus.filter(group, search_text, { tmp = fuzzaldrinPlus.filter(group, search_text, {
key: this.options.keys key: this.options.keys,
}); });
if (tmp.length) { if (tmp.length) {
results[key] = tmp.map(function(item) { results[key] = tmp.map(function(item) {
...@@ -180,7 +205,10 @@ GitLabDropdownFilter = (function() { ...@@ -180,7 +205,10 @@ GitLabDropdownFilter = (function() {
elements.show().removeClass('option-hidden'); elements.show().removeClass('option-hidden');
} }
elements.parent().find('.dropdown-menu-empty-item').toggleClass('hidden', elements.is(':visible')); elements
.parent()
.find('.dropdown-menu-empty-item')
.toggleClass('hidden', elements.is(':visible'));
} }
}; };
...@@ -194,23 +222,26 @@ GitLabDropdownRemote = (function() { ...@@ -194,23 +222,26 @@ GitLabDropdownRemote = (function() {
} }
GitLabDropdownRemote.prototype.execute = function() { GitLabDropdownRemote.prototype.execute = function() {
if (typeof this.dataEndpoint === "string") { if (typeof this.dataEndpoint === 'string') {
return this.fetchData(); return this.fetchData();
} else if (typeof this.dataEndpoint === "function") { } else if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) { if (this.options.beforeSend) {
this.options.beforeSend(); this.options.beforeSend();
} }
return this.dataEndpoint("", (function(_this) { return this.dataEndpoint(
// Fetch the data by calling the data funcfion '',
return function(data) { (function(_this) {
if (_this.options.success) { // Fetch the data by calling the data funcfion
_this.options.success(data); return function(data) {
} if (_this.options.success) {
if (_this.options.beforeSend) { _this.options.success(data);
return _this.options.beforeSend(); }
} if (_this.options.beforeSend) {
}; return _this.options.beforeSend();
})(this)); }
};
})(this),
);
} }
}; };
...@@ -220,33 +251,41 @@ GitLabDropdownRemote = (function() { ...@@ -220,33 +251,41 @@ GitLabDropdownRemote = (function() {
} }
// Fetch the data through ajax if the data is a string // Fetch the data through ajax if the data is a string
return axios.get(this.dataEndpoint) return axios.get(this.dataEndpoint).then(({ data }) => {
.then(({ data }) => { if (this.options.success) {
if (this.options.success) { return this.options.success(data);
return this.options.success(data); }
} });
});
}; };
return GitLabDropdownRemote; return GitLabDropdownRemote;
})(); })();
GitLabDropdown = (function() { GitLabDropdown = (function() {
var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex; var ACTIVE_CLASS,
FILTER_INPUT,
NO_FILTER_INPUT,
INDETERMINATE_CLASS,
LOADING_CLASS,
PAGE_TWO_CLASS,
NON_SELECTABLE_CLASSES,
SELECTABLE_CLASSES,
CURSOR_SELECT_SCROLL_PADDING,
currentIndex;
LOADING_CLASS = "is-loading"; LOADING_CLASS = 'is-loading';
PAGE_TWO_CLASS = "is-page-two"; PAGE_TWO_CLASS = 'is-page-two';
ACTIVE_CLASS = "is-active"; ACTIVE_CLASS = 'is-active';
INDETERMINATE_CLASS = "is-indeterminate"; INDETERMINATE_CLASS = 'is-indeterminate';
currentIndex = -1; currentIndex = -1;
NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item'; NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; SELECTABLE_CLASSES = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ', .option-hidden)';
CURSOR_SELECT_SCROLL_PADDING = 5; CURSOR_SELECT_SCROLL_PADDING = 5;
...@@ -263,15 +302,15 @@ GitLabDropdown = (function() { ...@@ -263,15 +302,15 @@ GitLabDropdown = (function() {
this.opened = this.opened.bind(this); this.opened = this.opened.bind(this);
this.shouldPropagate = this.shouldPropagate.bind(this); this.shouldPropagate = this.shouldPropagate.bind(this);
self = this; self = this;
selector = $(this.el).data("target"); selector = $(this.el).data('target');
this.dropdown = selector != null ? $(selector) : $(this.el).parent(); this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults // Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT); this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight; this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null this.icon = !!this.options.icon;
? this.options.filterInputBlur this.filterInputBlur =
: true; this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one // If no input is passed create a default one
self = this; self = this;
// If selector was passed // If selector was passed
...@@ -296,11 +335,17 @@ GitLabDropdown = (function() { ...@@ -296,11 +335,17 @@ GitLabDropdown = (function() {
_this.fullData = data; _this.fullData = data;
_this.parseData(_this.fullData); _this.parseData(_this.fullData);
_this.focusTextInput(); _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { if (
_this.options.filterable &&
_this.filter &&
_this.filter.input &&
_this.filter.input.val() &&
_this.filter.input.val().trim() !== ''
) {
return _this.filter.input.trigger('input'); return _this.filter.input.trigger('input');
} }
}; };
// Remote data // Remote data
})(this), })(this),
instance: this, instance: this,
}); });
...@@ -325,7 +370,7 @@ GitLabDropdown = (function() { ...@@ -325,7 +370,7 @@ GitLabDropdown = (function() {
return function() { return function() {
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')'; selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) { if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = '.dropdown-page-one ' + selector;
} }
return $(selector, this.instance.dropdown); return $(selector, this.instance.dropdown);
}; };
...@@ -341,80 +386,97 @@ GitLabDropdown = (function() { ...@@ -341,80 +386,97 @@ GitLabDropdown = (function() {
if (_this.filterInput.val() !== '') { if (_this.filterInput.val() !== '') {
selector = SELECTABLE_CLASSES; selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) { if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = '.dropdown-page-one ' + selector;
} }
if ($(_this.el).is('input')) { if ($(_this.el).is('input')) {
currentIndex = -1; currentIndex = -1;
} else { } else {
$(selector, _this.dropdown).first().find('a').addClass('is-focused'); $(selector, _this.dropdown)
.first()
.find('a')
.addClass('is-focused');
currentIndex = 0; currentIndex = 0;
} }
} }
}; };
})(this) })(this),
}); });
} }
// Event listeners // Event listeners
this.dropdown.on("shown.bs.dropdown", this.opened); this.dropdown.on('shown.bs.dropdown', this.opened);
this.dropdown.on("hidden.bs.dropdown", this.hidden); this.dropdown.on('hidden.bs.dropdown', this.hidden);
$(this.el).on("update.label", this.updateLabel); $(this.el).on('update.label', this.updateLabel);
this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate); this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
this.dropdown.on('keyup', (function(_this) { this.dropdown.on(
return function(e) { 'keyup',
// Escape key (function(_this) {
if (e.which === 27) { return function(e) {
return $('.dropdown-menu-close', _this.dropdown).trigger('click'); // Escape key
} if (e.which === 27) {
}; return $('.dropdown-menu-close', _this.dropdown).trigger('click');
})(this));
this.dropdown.on('blur', 'a', (function(_this) {
return function(e) {
var $dropdownMenu, $relatedTarget;
if (e.relatedTarget != null) {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
return _this.dropdown.removeClass('show');
} }
} };
}; })(this),
})(this)); );
if (this.dropdown.find(".dropdown-toggle-page").length) { this.dropdown.on(
this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) { 'blur',
'a',
(function(_this) {
return function(e) { return function(e) {
e.preventDefault(); var $dropdownMenu, $relatedTarget;
e.stopPropagation(); if (e.relatedTarget != null) {
return _this.togglePage(); $relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
return _this.dropdown.removeClass('show');
}
}
}; };
})(this)); })(this),
);
if (this.dropdown.find('.dropdown-toggle-page').length) {
this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on(
'click',
(function(_this) {
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.togglePage();
};
})(this),
);
} }
if (this.options.selectable) { if (this.options.selectable) {
selector = ".dropdown-content a"; selector = '.dropdown-content a';
if (this.dropdown.find(".dropdown-toggle-page").length) { if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one .dropdown-content a"; selector = '.dropdown-page-one .dropdown-content a';
} }
this.dropdown.on("click", selector, function(e) { this.dropdown.on(
var $el, selected, selectedObj, isMarking; 'click',
$el = $(e.currentTarget); selector,
selected = self.rowClicked($el); function(e) {
selectedObj = selected ? selected[0] : null; var $el, selected, selectedObj, isMarking;
isMarking = selected ? selected[1] : null; $el = $(e.currentTarget);
if (this.options.clicked) { selected = self.rowClicked($el);
this.options.clicked.call(this, { selectedObj = selected ? selected[0] : null;
selectedObj, isMarking = selected ? selected[1] : null;
$el, if (this.options.clicked) {
e, this.options.clicked.call(this, {
isMarking, selectedObj,
}); $el,
} e,
isMarking,
});
}
// Update label right after all modifications in dropdown has been done // Update label right after all modifications in dropdown has been done
if (this.options.toggleLabel) { if (this.options.toggleLabel) {
this.updateLabel(selectedObj, $el, this); this.updateLabel(selectedObj, $el, this);
} }
$el.trigger('blur'); $el.trigger('blur');
}.bind(this)); }.bind(this),
);
} }
} }
...@@ -452,10 +514,15 @@ GitLabDropdown = (function() { ...@@ -452,10 +514,15 @@ GitLabDropdown = (function() {
html = []; html = [];
for (name in data) { for (name in data) {
groupData = data[name]; groupData = data[name];
html.push(this.renderItem({ html.push(
header: name this.renderItem(
// Add header for each group {
}, name)); header: name,
// Add header for each group
},
name,
),
);
this.renderData(groupData, name).map(function(item) { this.renderData(groupData, name).map(function(item) {
return html.push(item); return html.push(item);
}); });
...@@ -474,20 +541,25 @@ GitLabDropdown = (function() { ...@@ -474,20 +541,25 @@ GitLabDropdown = (function() {
if (group == null) { if (group == null) {
group = false; group = false;
} }
return data.map((function(_this) { return data.map(
return function(obj, index) { (function(_this) {
return _this.renderItem(obj, group, index); return function(obj, index) {
}; return _this.renderItem(obj, group, index);
})(this)); };
})(this),
);
}; };
GitLabDropdown.prototype.shouldPropagate = function(e) { GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target; var $target;
if (this.options.multiSelect || this.options.shouldPropagate === false) { if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target); $target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') && if (
!$target.hasClass('dropdown-menu-close-icon') && $target &&
!$target.data('isLink')) { !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
!$target.data('isLink')
) {
e.stopPropagation(); e.stopPropagation();
return false; return false;
} else { } else {
...@@ -497,9 +569,11 @@ GitLabDropdown = (function() { ...@@ -497,9 +569,11 @@ GitLabDropdown = (function() {
}; };
GitLabDropdown.prototype.filteredFullData = function() { GitLabDropdown.prototype.filteredFullData = function() {
return this.fullData.filter(r => typeof r === 'object' return this.fullData.filter(
&& !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') r =>
&& !Object.prototype.hasOwnProperty.call(r, 'header') typeof r === 'object' &&
!Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
!Object.prototype.hasOwnProperty.call(r, 'header'),
); );
}; };
...@@ -522,11 +596,16 @@ GitLabDropdown = (function() { ...@@ -522,11 +596,16 @@ GitLabDropdown = (function() {
// matches the correct layout // matches the correct layout
const inputValue = this.filterInput.val(); const inputValue = this.filterInput.val();
if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) { if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this)); this.options.processData.call(
this.options,
inputValue,
this.filteredFullData(),
this.parseData.bind(this),
);
} }
contentHtml = $('.dropdown-content', this.dropdown).html(); contentHtml = $('.dropdown-content', this.dropdown).html();
if (this.remote && contentHtml === "") { if (this.remote && contentHtml === '') {
this.remote.execute(); this.remote.execute();
} else { } else {
this.focusTextInput(); this.focusTextInput();
...@@ -555,11 +634,11 @@ GitLabDropdown = (function() { ...@@ -555,11 +634,11 @@ GitLabDropdown = (function() {
var $input; var $input;
this.resetRows(); this.resetRows();
this.removeArrayKeyEvent(); this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field"); $input = this.dropdown.find('.dropdown-input-field');
if (this.options.filterable) { if (this.options.filterable) {
$input.blur(); $input.blur();
} }
if (this.dropdown.find(".dropdown-toggle-page").length) { if (this.dropdown.find('.dropdown-toggle-page').length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
} }
if (this.options.hidden) { if (this.options.hidden) {
...@@ -601,7 +680,7 @@ GitLabDropdown = (function() { ...@@ -601,7 +680,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.clearMenu = function() { GitLabDropdown.prototype.clearMenu = function() {
var selector; var selector;
selector = '.dropdown-content'; selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) { if (this.dropdown.find('.dropdown-toggle-page').length) {
if (this.options.containerSelector) { if (this.options.containerSelector) {
selector = this.options.containerSelector; selector = this.options.containerSelector;
} else { } else {
...@@ -619,7 +698,7 @@ GitLabDropdown = (function() { ...@@ -619,7 +698,7 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id; value = this.options.id ? this.options.id(data) : data.id;
if (value) { if (value) {
value = value.toString().replace(/'/g, '\\\''); value = value.toString().replace(/'/g, "\\'");
} }
} }
...@@ -676,21 +755,27 @@ GitLabDropdown = (function() { ...@@ -676,21 +755,27 @@ GitLabDropdown = (function() {
text = data.text != null ? data.text : ''; text = data.text != null ? data.text : '';
} }
if (this.highlight) { if (this.highlight) {
text = this.highlightTextMatches(text, this.filterInput.val()); text = data.template
? this.highlightTemplate(text, data.template)
: this.highlightTextMatches(text, this.filterInput.val());
} }
// Create the list item & the link // Create the list item & the link
var link = document.createElement('a'); var link = document.createElement('a');
link.href = url; link.href = url;
if (this.highlight) { if (this.icon) {
text = `<span>${text}</span>`;
link.classList.add('d-flex', 'align-items-center');
link.innerHTML = data.icon ? data.icon + text : text;
} else if (this.highlight) {
link.innerHTML = text; link.innerHTML = text;
} else { } else {
link.textContent = text; link.textContent = text;
} }
if (selected) { if (selected) {
link.className = 'is-active'; link.classList.add('is-active');
} }
if (group) { if (group) {
...@@ -703,17 +788,24 @@ GitLabDropdown = (function() { ...@@ -703,17 +788,24 @@ GitLabDropdown = (function() {
return html; return html;
}; };
GitLabDropdown.prototype.highlightTemplate = function(text, template) {
return `"<b>${_.escape(text)}</b>" ${template}`;
};
GitLabDropdown.prototype.highlightTextMatches = function(text, term) { GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
const occurrences = fuzzaldrinPlus.match(text, term); const occurrences = fuzzaldrinPlus.match(text, term);
const { indexOf } = []; const { indexOf } = [];
return text.split('').map(function(character, i) { return text
if (indexOf.call(occurrences, i) !== -1) { .split('')
return "<b>" + character + "</b>"; .map(function(character, i) {
} else { if (indexOf.call(occurrences, i) !== -1) {
return character; return '<b>' + character + '</b>';
} } else {
}).join(''); return character;
}
})
.join('');
}; };
GitLabDropdown.prototype.noResults = function() { GitLabDropdown.prototype.noResults = function() {
...@@ -748,13 +840,15 @@ GitLabDropdown = (function() { ...@@ -748,13 +840,15 @@ GitLabDropdown = (function() {
} }
field = []; field = [];
value = this.options.id value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
? this.options.id(selectedObject, el)
: selectedObject.id;
if (isInput) { if (isInput) {
field = $(this.el); field = $(this.el);
} else if (value != null) { } else if (value != null) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); field = this.dropdown
.parent()
.find(
"input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, "\\'") + "']",
);
} }
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) { if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
...@@ -780,9 +874,12 @@ GitLabDropdown = (function() { ...@@ -780,9 +874,12 @@ GitLabDropdown = (function() {
} else { } else {
isMarking = true; isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS); this.dropdown.find('.' + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) { if (!isInput) {
this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); this.dropdown
.parent()
.find("input[name='" + fieldName + "']")
.remove();
} }
} }
if (field && field.length && value == null) { if (field && field.length && value == null) {
...@@ -823,13 +920,16 @@ GitLabDropdown = (function() { ...@@ -823,13 +920,16 @@ GitLabDropdown = (function() {
$('input[name="' + fieldName + '"]').remove(); $('input[name="' + fieldName + '"]').remove();
} }
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); $input = $('<input>')
.attr('type', 'hidden')
.attr('name', fieldName)
.val(value);
if (this.options.inputId != null) { if (this.options.inputId != null) {
$input.attr('id', this.options.inputId); $input.attr('id', this.options.inputId);
} }
if (this.options.multiSelect) { if (this.options.multiSelect) {
Object.keys(selectedObject).forEach((attribute) => { Object.keys(selectedObject).forEach(attribute => {
$input.attr(`data-${attribute}`, selectedObject[attribute]); $input.attr(`data-${attribute}`, selectedObject[attribute]);
}); });
} }
...@@ -844,13 +944,13 @@ GitLabDropdown = (function() { ...@@ -844,13 +944,13 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.selectRowAtIndex = function(index) { GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector; var $el, selector;
// If we pass an option index // If we pass an option index
if (typeof index !== "undefined") { if (typeof index !== 'undefined') {
selector = SELECTABLE_CLASSES + ":eq(" + index + ") a"; selector = SELECTABLE_CLASSES + ':eq(' + index + ') a';
} else { } else {
selector = ".dropdown-content .is-focused"; selector = '.dropdown-content .is-focused';
} }
if (this.dropdown.find(".dropdown-toggle-page").length) { if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = '.dropdown-page-one ' + selector;
} }
// simulate a click on the first link // simulate a click on the first link
$el = $(selector, this.dropdown); $el = $(selector, this.dropdown);
...@@ -867,44 +967,47 @@ GitLabDropdown = (function() { ...@@ -867,44 +967,47 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.addArrowKeyEvent = function() { GitLabDropdown.prototype.addArrowKeyEvent = function() {
var $input, ARROW_KEY_CODES, selector; var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40]; ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field"); $input = this.dropdown.find('.dropdown-input-field');
selector = SELECTABLE_CLASSES; selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) { if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = '.dropdown-page-one ' + selector;
} }
return $('body').on('keydown', (function(_this) { return $('body').on(
return function(e) { 'keydown',
var $listItems, PREV_INDEX, currentKeyCode; (function(_this) {
currentKeyCode = e.which; return function(e) {
if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { var $listItems, PREV_INDEX, currentKeyCode;
e.preventDefault(); currentKeyCode = e.which;
e.stopImmediatePropagation(); if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
PREV_INDEX = currentIndex; e.preventDefault();
$listItems = $(selector, _this.dropdown); e.stopImmediatePropagation();
// if @options.filterable PREV_INDEX = currentIndex;
// $input.blur() $listItems = $(selector, _this.dropdown);
if (currentKeyCode === 40) { // if @options.filterable
// Move down // $input.blur()
if (currentIndex < ($listItems.length - 1)) { if (currentKeyCode === 40) {
currentIndex += 1; // Move down
if (currentIndex < $listItems.length - 1) {
currentIndex += 1;
}
} else if (currentKeyCode === 38) {
// Move up
if (currentIndex > 0) {
currentIndex -= 1;
}
} }
} else if (currentKeyCode === 38) { if (currentIndex !== PREV_INDEX) {
// Move up _this.highlightRowAtIndex($listItems, currentIndex);
if (currentIndex > 0) {
currentIndex -= 1;
} }
return false;
} }
if (currentIndex !== PREV_INDEX) { if (currentKeyCode === 13 && currentIndex !== -1) {
_this.highlightRowAtIndex($listItems, currentIndex); e.preventDefault();
_this.selectRowAtIndex();
} }
return false; };
} })(this),
if (currentKeyCode === 13 && currentIndex !== -1) { );
e.preventDefault();
_this.selectRowAtIndex();
}
};
})(this));
}; };
GitLabDropdown.prototype.removeArrayKeyEvent = function() { GitLabDropdown.prototype.removeArrayKeyEvent = function() {
...@@ -917,12 +1020,25 @@ GitLabDropdown = (function() { ...@@ -917,12 +1020,25 @@ GitLabDropdown = (function() {
}; };
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) { GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop; var $dropdownContent,
$listItem,
dropdownContentBottom,
dropdownContentHeight,
dropdownContentTop,
dropdownScrollTop,
listItemBottom,
listItemHeight,
listItemTop;
if (!$listItems) {
$listItems = $(SELECTABLE_CLASSES, this.dropdown);
}
// Remove the class for the previously focused row // Remove the class for the previously focused row
$('.is-focused', this.dropdown).removeClass('is-focused'); $('.is-focused', this.dropdown).removeClass('is-focused');
// Update the class for the row at the specific index // Update the class for the row at the specific index
$listItem = $listItems.eq(index); $listItem = $listItems.eq(index);
$listItem.find('a:first-child').addClass("is-focused"); $listItem.find('a:first-child').addClass('is-focused');
// Dropdown content scroll area // Dropdown content scroll area
$dropdownContent = $listItem.closest('.dropdown-content'); $dropdownContent = $listItem.closest('.dropdown-content');
dropdownScrollTop = $dropdownContent.scrollTop(); dropdownScrollTop = $dropdownContent.scrollTop();
...@@ -936,15 +1052,19 @@ GitLabDropdown = (function() { ...@@ -936,15 +1052,19 @@ GitLabDropdown = (function() {
if (!index) { if (!index) {
// Scroll the dropdown content to the top // Scroll the dropdown content to the top
$dropdownContent.scrollTop(0); $dropdownContent.scrollTop(0);
} else if (index === ($listItems.length - 1)) { } else if (index === $listItems.length - 1) {
// Scroll the dropdown content to the bottom // Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) { } else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
// Scroll the dropdown content down // Scroll the dropdown content down
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING); $dropdownContent.scrollTop(
} else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) { listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
);
} else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
// Scroll the dropdown content up // Scroll the dropdown content up
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING); return $dropdownContent.scrollTop(
listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
);
} }
}; };
...@@ -965,7 +1085,9 @@ GitLabDropdown = (function() { ...@@ -965,7 +1085,9 @@ GitLabDropdown = (function() {
toggleText = this.options.updateLabel; toggleText = this.options.updateLabel;
} }
return $(this.el).find(".dropdown-toggle-text").text(toggleText); return $(this.el)
.find('.dropdown-toggle-text')
.text(toggleText);
}; };
GitLabDropdown.prototype.clearField = function(field, isInput) { GitLabDropdown.prototype.clearField = function(field, isInput) {
......
/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, quotes, class-methods-use-this, no-lonely-if, no-else-return, vars-on-top, max-len */ /* eslint-disable no-return-assign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top, max-len */
import $ from 'jquery'; import $ from 'jquery';
import { escape, throttle } from 'underscore';
import { s__, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import DropdownUtils from './filtered_search/dropdown_utils'; import DropdownUtils from './filtered_search/dropdown_utils';
import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; import {
isInGroupsPage,
isInProjectPage,
getGroupSlug,
getProjectSlug,
spriteIcon,
} from './lib/utils/common_utils';
/** /**
* Search input in top navigation bar. * Search input in top navigation bar.
...@@ -52,6 +61,7 @@ function setSearchOptions() { ...@@ -52,6 +61,7 @@ function setSearchOptions() {
if ($dashboardOptionsDataEl.length) { if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = { gl.dashboardOptions = {
name: s__('SearchAutocomplete|All GitLab'),
issuesPath: $dashboardOptionsDataEl.data('issuesPath'), issuesPath: $dashboardOptionsDataEl.data('issuesPath'),
mrPath: $dashboardOptionsDataEl.data('mrPath'), mrPath: $dashboardOptionsDataEl.data('mrPath'),
}; };
...@@ -69,8 +79,8 @@ export default class SearchAutocomplete { ...@@ -69,8 +79,8 @@ export default class SearchAutocomplete {
this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || ''); this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown'); this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownMenu = this.dropdown.find('.dropdown-menu');
this.dropdownContent = this.dropdown.find('.dropdown-content'); this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
this.scopeInputEl = this.getElement('#scope'); this.scopeInputEl = this.getElement('#scope');
this.searchInput = this.getElement('.search-input'); this.searchInput = this.getElement('.search-input');
this.projectInputEl = this.getElement('#search_project_id'); this.projectInputEl = this.getElement('#search_project_id');
...@@ -78,6 +88,7 @@ export default class SearchAutocomplete { ...@@ -78,6 +88,7 @@ export default class SearchAutocomplete {
this.searchCodeInputEl = this.getElement('#search_code'); this.searchCodeInputEl = this.getElement('#search_code');
this.repositoryInputEl = this.getElement('#repository_ref'); this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input'); this.clearInput = this.getElement('.js-clear-input');
this.scrollFadeInitialized = false;
this.saveOriginalState(); this.saveOriginalState();
// Only when user is logged in // Only when user is logged in
...@@ -98,17 +109,18 @@ export default class SearchAutocomplete { ...@@ -98,17 +109,18 @@ export default class SearchAutocomplete {
this.onSearchInputFocus = this.onSearchInputFocus.bind(this); this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
this.setScrollFade = this.setScrollFade.bind(this);
} }
getElement(selector) { getElement(selector) {
return this.wrap.find(selector); return this.wrap.find(selector);
} }
saveOriginalState() { saveOriginalState() {
return this.originalState = this.serializeState(); return (this.originalState = this.serializeState());
} }
saveTextLength() { saveTextLength() {
return this.lastTextLength = this.searchInput.val().length; return (this.lastTextLength = this.searchInput.val().length);
} }
createAutocomplete() { createAutocomplete() {
...@@ -117,6 +129,7 @@ export default class SearchAutocomplete { ...@@ -117,6 +129,7 @@ export default class SearchAutocomplete {
filterable: true, filterable: true,
filterRemote: true, filterRemote: true,
highlight: true, highlight: true,
icon: true,
enterCallback: false, enterCallback: false,
filterInput: 'input#search', filterInput: 'input#search',
search: { search: {
...@@ -154,60 +167,87 @@ export default class SearchAutocomplete { ...@@ -154,60 +167,87 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true; this.loadingSuggestions = true;
return axios.get(this.autocompletePath, { return axios
params: { .get(this.autocompletePath, {
project_id: this.projectId, params: {
project_ref: this.projectRef, project_id: this.projectId,
term: term, project_ref: this.projectRef,
}, term: term,
}).then((response) => { },
// Hide dropdown menu if no suggestions returns })
if (!response.data.length) { .then(response => {
this.disableAutocomplete(); // Hide dropdown menu if no suggestions returns
return; if (!response.data.length) {
} this.disableAutocomplete();
return;
}
const data = []; const data = [];
// List results // List results
let firstCategory = true; let firstCategory = true;
let lastCategory; let lastCategory;
for (let i = 0, len = response.data.length; i < len; i += 1) { for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i]; const suggestion = response.data[i];
// Add group header before list each group // Add group header before list each group
if (lastCategory !== suggestion.category) { if (lastCategory !== suggestion.category) {
if (!firstCategory) { if (!firstCategory) {
data.push('separator'); data.push('separator');
} }
if (firstCategory) { if (firstCategory) {
firstCategory = false; firstCategory = false;
}
data.push({
header: suggestion.category,
});
lastCategory = suggestion.category;
} }
data.push({ data.push({
header: suggestion.category, id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
icon: this.getAvatar(suggestion),
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
}); });
lastCategory = suggestion.category;
} }
data.push({ // Add option to proceed with the search
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`, if (data.length) {
category: suggestion.category, const icon = spriteIcon('search', 's16 inline-search-icon');
text: suggestion.label, let template;
url: suggestion.url,
});
}
// Add option to proceed with the search
if (data.length) {
data.push('separator');
data.push({
text: `Result name contains "${term}"`,
url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
callback(data); if (this.projectInputEl.val()) {
template = s__('SearchAutocomplete|in this project');
}
if (this.groupInputEl.val()) {
template = s__('SearchAutocomplete|in this group');
}
this.loadingSuggestions = false; data.unshift('separator');
}).catch(() => { data.unshift({
this.loadingSuggestions = false; icon,
}); text: term,
template: s__('SearchAutocomplete|in all GitLab'),
url: `/search?search=${term}`,
});
if (template) {
data.unshift({
icon,
text: term,
template,
url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
}
callback(data);
this.loadingSuggestions = false;
this.highlightFirstRow();
this.setScrollFade();
})
.catch(() => {
this.loadingSuggestions = false;
});
} }
getCategoryContents() { getCategoryContents() {
...@@ -236,21 +276,21 @@ export default class SearchAutocomplete { ...@@ -236,21 +276,21 @@ export default class SearchAutocomplete {
const issueItems = [ const issueItems = [
{ {
text: 'Issues assigned to me', text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_id=${userId}`, url: `${issuesPath}/?assignee_id=${userId}`,
}, },
{ {
text: "Issues I've created", text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_id=${userId}`, url: `${issuesPath}/?author_id=${userId}`,
}, },
]; ];
const mergeRequestItems = [ const mergeRequestItems = [
{ {
text: 'Merge requests assigned to me', text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_id=${userId}`, url: `${mrPath}/?assignee_id=${userId}`,
}, },
{ {
text: "Merge requests I've created", text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_id=${userId}`, url: `${mrPath}/?author_id=${userId}`,
}, },
]; ];
...@@ -259,7 +299,7 @@ export default class SearchAutocomplete { ...@@ -259,7 +299,7 @@ export default class SearchAutocomplete {
if (issuesDisabled) { if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems); items = baseItems.concat(mergeRequestItems);
} else { } else {
items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems); items = baseItems.concat(...issueItems, ...mergeRequestItems);
} }
return items; return items;
} }
...@@ -272,8 +312,6 @@ export default class SearchAutocomplete { ...@@ -272,8 +312,6 @@ export default class SearchAutocomplete {
search_code: this.searchCodeInputEl.val(), search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(), repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(), scope: this.scopeInputEl.val(),
// Location badge
_location: this.locationBadgeEl.text(),
}; };
} }
...@@ -283,10 +321,12 @@ export default class SearchAutocomplete { ...@@ -283,10 +321,12 @@ export default class SearchAutocomplete {
this.searchInput.on('focus', this.onSearchInputFocus); this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur); this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick); this.clearInput.on('click', this.onClearInputClick);
this.locationBadgeEl.on('click', () => this.searchInput.focus()); this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
} }
enableAutocomplete() { enableAutocomplete() {
this.setScrollFade();
// No need to enable anything if user is not logged in // No need to enable anything if user is not logged in
if (!gon.current_user_id) { if (!gon.current_user_id) {
return; return;
...@@ -308,10 +348,6 @@ export default class SearchAutocomplete { ...@@ -308,10 +348,6 @@ export default class SearchAutocomplete {
onSearchInputKeyUp(e) { onSearchInputKeyUp(e) {
switch (e.keyCode) { switch (e.keyCode) {
case KEYCODE.BACKSPACE: case KEYCODE.BACKSPACE:
// when trying to remove the location badge
if (this.lastTextLength === 0 && this.badgePresent()) {
this.removeLocationBadge();
}
// When removing the last character and no badge is present // When removing the last character and no badge is present
if (this.lastTextLength === 1) { if (this.lastTextLength === 1) {
this.disableAutocomplete(); this.disableAutocomplete();
...@@ -372,37 +408,13 @@ export default class SearchAutocomplete { ...@@ -372,37 +408,13 @@ export default class SearchAutocomplete {
} }
} }
addLocationBadge(item) {
var badgeText, category, value;
category = item.category != null ? item.category + ": " : '';
value = item.value != null ? item.value : '';
badgeText = "" + category + value;
this.locationBadgeEl.text(badgeText).show();
return this.wrap.addClass('has-location-badge');
}
hasLocationBadge() {
return this.wrap.is('.has-location-badge');
}
restoreOriginalState() { restoreOriginalState() {
var i, input, inputs, len; var i, input, inputs, len;
inputs = Object.keys(this.originalState); inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i += 1) { for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i]; input = inputs[i];
this.getElement("#" + input).val(this.originalState[input]); this.getElement('#' + input).val(this.originalState[input]);
} }
if (this.originalState._location === '') {
return this.locationBadgeEl.hide();
} else {
return this.addLocationBadge({
value: this.originalState._location,
});
}
}
badgePresent() {
return this.locationBadgeEl.length;
} }
resetSearchState() { resetSearchState() {
...@@ -411,22 +423,11 @@ export default class SearchAutocomplete { ...@@ -411,22 +423,11 @@ export default class SearchAutocomplete {
results = []; results = [];
for (i = 0, len = inputs.length; i < len; i += 1) { for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i]; input = inputs[i];
// _location isnt a input results.push(this.getElement('#' + input).val(''));
if (input === '_location') {
break;
}
results.push(this.getElement("#" + input).val(''));
} }
return results; return results;
} }
removeLocationBadge() {
this.locationBadgeEl.hide();
this.resetSearchState();
this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete();
}
disableAutocomplete() { disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) { if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('disabled'); this.searchInput.addClass('disabled');
...@@ -444,23 +445,57 @@ export default class SearchAutocomplete { ...@@ -444,23 +445,57 @@ export default class SearchAutocomplete {
onClick(item, $el, e) { onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) { if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault(); if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) { if (item.category === 'Projects') {
if (item.category === 'Projects') { this.projectInputEl.val(item.id);
this.projectInputEl.val(item.id); }
this.addLocationBadge({ if (item.category === 'Groups') {
value: 'This project', this.groupInputEl.val(item.id);
});
}
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
this.addLocationBadge({
value: 'This group',
});
}
} }
$el.removeClass('is-active'); $el.removeClass('is-active');
this.disableAutocomplete(); this.disableAutocomplete();
return this.searchInput.val('').focus(); return this.searchInput.val('').focus();
} }
} }
highlightFirstRow() {
this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
}
getAvatar(item) {
if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
return false;
}
const { label, id } = item;
const avatarUrl = item.avatar_url;
const avatar = avatarUrl
? `<img class="search-item-avatar" src="${avatarUrl}" />`
: `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
escape(label),
)}</div>`;
return avatar;
}
isScrolledUp() {
const el = this.dropdownContent[0];
const currentPosition = this.contentClientHeight + el.scrollTop;
return currentPosition < this.maxPosition;
}
initScrollFade() {
const el = this.dropdownContent[0];
this.scrollFadeInitialized = true;
this.contentClientHeight = el.clientHeight;
this.maxPosition = el.scrollHeight;
this.dropdownMenu.addClass('dropdown-content-faded-mask');
}
setScrollFade() {
this.initScrollFade();
this.dropdownMenu.toggleClass('fade-out', !this.isScrolledUp());
}
} }
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
*/ */
@mixin gitlab-theme( @mixin gitlab-theme(
$location-badge-color,
$search-and-nav-links, $search-and-nav-links,
$active-tab-border, $active-tab-border,
$border-and-box-shadow, $border-and-box-shadow,
...@@ -119,12 +118,6 @@ ...@@ -119,12 +118,6 @@
} }
} }
.location-badge {
color: $location-badge-color;
background-color: rgba($search-and-nav-links, 0.1);
border-right: 1px solid $sidebar-text;
}
.search-input::placeholder { .search-input::placeholder {
color: rgba($search-and-nav-links, 0.8); color: rgba($search-and-nav-links, 0.8);
} }
...@@ -141,10 +134,6 @@ ...@@ -141,10 +134,6 @@
background-color: $white-light; background-color: $white-light;
} }
.location-badge {
color: $gl-text-color;
}
.search-input-wrap { .search-input-wrap {
.search-icon { .search-icon {
fill: rgba($search-and-nav-links, 0.8); fill: rgba($search-and-nav-links, 0.8);
...@@ -200,7 +189,6 @@ ...@@ -200,7 +189,6 @@
body { body {
&.ui-indigo { &.ui-indigo {
@include gitlab-theme( @include gitlab-theme(
$indigo-100,
$indigo-200, $indigo-200,
$indigo-500, $indigo-500,
$indigo-700, $indigo-700,
...@@ -212,7 +200,6 @@ body { ...@@ -212,7 +200,6 @@ body {
&.ui-light-indigo { &.ui-light-indigo {
@include gitlab-theme( @include gitlab-theme(
$indigo-100,
$indigo-200, $indigo-200,
$indigo-500, $indigo-500,
$indigo-500, $indigo-500,
...@@ -224,7 +211,6 @@ body { ...@@ -224,7 +211,6 @@ body {
&.ui-blue { &.ui-blue {
@include gitlab-theme( @include gitlab-theme(
$theme-blue-100,
$theme-blue-200, $theme-blue-200,
$theme-blue-500, $theme-blue-500,
$theme-blue-700, $theme-blue-700,
...@@ -236,7 +222,6 @@ body { ...@@ -236,7 +222,6 @@ body {
&.ui-light-blue { &.ui-light-blue {
@include gitlab-theme( @include gitlab-theme(
$theme-light-blue-100,
$theme-light-blue-200, $theme-light-blue-200,
$theme-light-blue-500, $theme-light-blue-500,
$theme-light-blue-500, $theme-light-blue-500,
...@@ -248,7 +233,6 @@ body { ...@@ -248,7 +233,6 @@ body {
&.ui-green { &.ui-green {
@include gitlab-theme( @include gitlab-theme(
$theme-green-100,
$theme-green-200, $theme-green-200,
$theme-green-500, $theme-green-500,
$theme-green-700, $theme-green-700,
...@@ -260,7 +244,6 @@ body { ...@@ -260,7 +244,6 @@ body {
&.ui-light-green { &.ui-light-green {
@include gitlab-theme( @include gitlab-theme(
$theme-green-100,
$theme-green-200, $theme-green-200,
$theme-green-500, $theme-green-500,
$theme-green-500, $theme-green-500,
...@@ -272,7 +255,6 @@ body { ...@@ -272,7 +255,6 @@ body {
&.ui-red { &.ui-red {
@include gitlab-theme( @include gitlab-theme(
$theme-red-100,
$theme-red-200, $theme-red-200,
$theme-red-500, $theme-red-500,
$theme-red-700, $theme-red-700,
...@@ -284,7 +266,6 @@ body { ...@@ -284,7 +266,6 @@ body {
&.ui-light-red { &.ui-light-red {
@include gitlab-theme( @include gitlab-theme(
$theme-light-red-100,
$theme-light-red-200, $theme-light-red-200,
$theme-light-red-500, $theme-light-red-500,
$theme-light-red-500, $theme-light-red-500,
...@@ -296,7 +277,6 @@ body { ...@@ -296,7 +277,6 @@ body {
&.ui-dark { &.ui-dark {
@include gitlab-theme( @include gitlab-theme(
$theme-gray-100,
$theme-gray-200, $theme-gray-200,
$theme-gray-500, $theme-gray-500,
$theme-gray-700, $theme-gray-700,
...@@ -308,7 +288,6 @@ body { ...@@ -308,7 +288,6 @@ body {
&.ui-light { &.ui-light {
@include gitlab-theme( @include gitlab-theme(
$theme-gray-900,
$theme-gray-700, $theme-gray-700,
$theme-gray-800, $theme-gray-800,
$theme-gray-700, $theme-gray-700,
...@@ -357,10 +336,6 @@ body { ...@@ -357,10 +336,6 @@ body {
&:hover { &:hover {
background-color: $white-light; background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-200; box-shadow: inset 0 0 0 1px $blue-200;
.location-badge {
box-shadow: inset 0 0 0 1px $blue-200;
}
} }
} }
...@@ -373,13 +348,6 @@ body { ...@@ -373,13 +348,6 @@ body {
color: $gl-text-color; color: $gl-text-color;
} }
} }
.location-badge {
color: $theme-gray-700;
box-shadow: inset 0 0 0 1px $border-color;
background-color: $nav-badge-bg;
border-right: 0;
}
} }
.nav-sidebar li.active { .nav-sidebar li.active {
......
...@@ -467,7 +467,8 @@ $award-emoji-positive-add-lines: #bb9c13; ...@@ -467,7 +467,8 @@ $award-emoji-positive-add-lines: #bb9c13;
*/ */
$search-input-border-color: rgba($blue-400, 0.8); $search-input-border-color: rgba($blue-400, 0.8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: 220px; $search-input-width: 240px;
$search-input-active-width: 320px;
$location-badge-active-bg: $blue-500; $location-badge-active-bg: $blue-500;
$location-icon-color: #e7e9ed; $location-icon-color: #e7e9ed;
......
$search-dropdown-max-height: 400px;
$search-avatar-size: 16px;
.search-results { .search-results {
.search-result-row { .search-result-row {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -24,8 +27,9 @@ ...@@ -24,8 +27,9 @@
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
} }
input[type="checkbox"]:hover { input[type='checkbox']:hover {
box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%); box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%),
0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
} }
.search { .search {
...@@ -40,24 +44,15 @@ input[type="checkbox"]:hover { ...@@ -40,24 +44,15 @@ input[type="checkbox"]:hover {
height: 32px; height: 32px;
border: 0; border: 0;
border-radius: $border-radius-default; border-radius: $border-radius-default;
transition: border-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration; transition: border-color ease-in-out $default-transition-duration,
background-color ease-in-out $default-transition-duration,
width ease-in-out $default-transition-duration;
&:hover { &:hover {
box-shadow: none; box-shadow: none;
} }
} }
.location-badge {
white-space: nowrap;
height: 32px;
font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: $border-radius-default 0 0 $border-radius-default;
transition: border-color ease-in-out $default-transition-duration;
}
.search-input { .search-input {
border: 0; border: 0;
font-size: 14px; font-size: 14px;
...@@ -104,17 +99,28 @@ input[type="checkbox"]:hover { ...@@ -104,17 +99,28 @@ input[type="checkbox"]:hover {
} }
.dropdown-header { .dropdown-header {
text-transform: uppercase; // Necessary because glDropdown doesn't support a second style of headers
font-size: 11px; font-weight: $gl-font-weight-bold;
// .dropdown-menu li has 1px side padding
padding: $gl-padding-8 17px;
color: $gl-text-color;
font-size: $gl-font-size;
line-height: 16px;
} }
// Custom dropdown positioning // Custom dropdown positioning
.dropdown-menu { .dropdown-menu {
left: -5px; left: -5px;
max-height: $search-dropdown-max-height;
overflow: auto;
@include media-breakpoint-up(xl) {
width: $search-input-active-width;
}
} }
.dropdown-content { .dropdown-content {
max-height: none; max-height: $search-dropdown-max-height - 18px;
} }
} }
...@@ -124,6 +130,10 @@ input[type="checkbox"]:hover { ...@@ -124,6 +130,10 @@ input[type="checkbox"]:hover {
border-color: $dropdown-input-focus-border; border-color: $dropdown-input-focus-border;
box-shadow: none; box-shadow: none;
@include media-breakpoint-up(xl) {
width: $search-input-active-width;
}
.search-input-wrap { .search-input-wrap {
.search-icon, .search-icon,
.clear-icon { .clear-icon {
...@@ -141,12 +151,6 @@ input[type="checkbox"]:hover { ...@@ -141,12 +151,6 @@ input[type="checkbox"]:hover {
color: $gl-text-color-tertiary; color: $gl-text-color-tertiary;
} }
} }
.location-badge {
transition: all $default-transition-duration;
background-color: $nav-badge-bg;
border-color: $border-color;
}
} }
&.has-value { &.has-value {
...@@ -160,10 +164,24 @@ input[type="checkbox"]:hover { ...@@ -160,10 +164,24 @@ input[type="checkbox"]:hover {
} }
} }
&.has-location-badge { .inline-search-icon {
.search-input-wrap { position: relative;
width: 68%; margin-right: 4px;
} color: $gl-text-color-secondary;
}
.identicon,
.search-item-avatar {
flex-basis: $search-avatar-size;
flex-shrink: 0;
margin-right: 4px;
}
.search-item-avatar {
width: $search-avatar-size;
height: $search-avatar-size;
border-radius: 50%;
border: 1px solid $avatar-border;
} }
} }
......
...@@ -82,16 +82,16 @@ module SearchHelper ...@@ -82,16 +82,16 @@ module SearchHelper
ref = @ref || @project.repository.root_ref ref = @ref || @project.repository.root_ref
[ [
{ category: "Current Project", label: "Files", url: project_tree_path(@project, ref) }, { category: "In this project", label: "Files", url: project_tree_path(@project, ref) },
{ category: "Current Project", label: "Commits", url: project_commits_path(@project, ref) }, { category: "In this project", label: "Commits", url: project_commits_path(@project, ref) },
{ category: "Current Project", label: "Network", url: project_network_path(@project, ref) }, { category: "In this project", label: "Network", url: project_network_path(@project, ref) },
{ category: "Current Project", label: "Graph", url: project_graph_path(@project, ref) }, { category: "In this project", label: "Graph", url: project_graph_path(@project, ref) },
{ category: "Current Project", label: "Issues", url: project_issues_path(@project) }, { category: "In this project", label: "Issues", url: project_issues_path(@project) },
{ category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) }, { category: "In this project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "Current Project", label: "Milestones", url: project_milestones_path(@project) }, { category: "In this project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "Current Project", label: "Snippets", url: project_snippets_path(@project) }, { category: "In this project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "Current Project", label: "Members", url: project_project_members_path(@project) }, { category: "In this project", label: "Members", url: project_project_members_path(@project) },
{ category: "Current Project", label: "Wiki", url: project_wikis_path(@project) } { category: "In this project", label: "Wiki", url: project_wikis_path(@project) }
] ]
else else
[] []
......
...@@ -6,21 +6,19 @@ ...@@ -6,21 +6,19 @@
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) } - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted? - if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? } - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }
.search.search-form{ class: "#{'has-location-badge' if label.present?}" } .search.search-form
= form_tag search_path, method: :get, class: 'form-inline' do |f| = form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container .search-input-container
- if label.present?
.location-badge= label
.search-input-wrap .search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } } .dropdown{ data: { url: search_autocomplete_path } }
= search_field_tag 'search', nil, placeholder: _('Search'), = search_field_tag 'search', nil, placeholder: _('Search or jump to…'),
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false, spellcheck: false,
tabindex: '1', tabindex: '1',
autocomplete: 'off', autocomplete: 'off',
data: { issues_path: issues_dashboard_path, data: { issues_path: issues_dashboard_path,
mr_path: merge_requests_dashboard_path }, mr_path: merge_requests_dashboard_path },
aria: { label: _('Search') } aria: { label: _('Search or jump to…') }
%button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } } %button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }
.dropdown-menu.dropdown-select .dropdown-menu.dropdown-select
= dropdown_content do = dropdown_content do
......
---
title: UX improvements to top nav search bar
merge_request: 20537
author:
type: changed
doc/user/search/img/project_search.png

40.9 KB | W: | H:

doc/user/search/img/project_search.png

86.9 KB | W: | H:

doc/user/search/img/project_search.png
doc/user/search/img/project_search.png
doc/user/search/img/project_search.png
doc/user/search/img/project_search.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -4606,12 +4606,39 @@ msgstr "" ...@@ -4606,12 +4606,39 @@ msgstr ""
msgid "Search milestones" msgid "Search milestones"
msgstr "" msgstr ""
msgid "Search or jump to…"
msgstr ""
msgid "Search project" msgid "Search project"
msgstr "" msgstr ""
msgid "Search users" msgid "Search users"
msgstr "" msgstr ""
msgid "SearchAutocomplete|All GitLab"
msgstr ""
msgid "SearchAutocomplete|Issues I've created"
msgstr ""
msgid "SearchAutocomplete|Issues assigned to me"
msgstr ""
msgid "SearchAutocomplete|Merge requests I've created"
msgstr ""
msgid "SearchAutocomplete|Merge requests assigned to me"
msgstr ""
msgid "SearchAutocomplete|in all GitLab"
msgstr ""
msgid "SearchAutocomplete|in this group"
msgstr ""
msgid "SearchAutocomplete|in this project"
msgstr ""
msgid "Seconds before reseting failure information" msgid "Seconds before reseting failure information"
msgstr "" msgstr ""
......
...@@ -62,10 +62,6 @@ describe 'User uses header search field' do ...@@ -62,10 +62,6 @@ describe 'User uses header search field' do
end end
end end
it 'contains location badge' do
expect(page).to have_selector('.has-location-badge')
end
context 'when clicking the search field', :js do context 'when clicking the search field', :js do
before do before do
page.find('#search').click page.find('#search').click
......
.search.search-form.has-location-badge .search.search-form
%form.navbar-form %form.form-inline
.search-input-container .search-input-container
%div.location-badge
This project
.search-input-wrap .search-input-wrap
.dropdown .dropdown
%input#search.search-input.dropdown-menu-toggle %input#search.search-input.dropdown-menu-toggle
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment