Commit efec7e08 authored by Dennis Tang's avatar Dennis Tang Committed by Phil Hughes

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

parent 8a1d55a3
/* 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 */
import $ from 'jquery';
......@@ -19,17 +19,23 @@ GitLabDropdownInput = (function() {
this.fieldName = this.options.fieldName || 'field-name';
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) {
$clearButton.on(
'click',
(function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('input').focus();
return _this.input
.val('')
.trigger('input')
.focus();
};
})(this));
})(this),
);
this.input
.on('keydown', function (e) {
.on('keydown', function(e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
......@@ -37,11 +43,15 @@ GitLabDropdownInput = (function() {
})
.on('input', function(e) {
var val = e.currentTarget.value || _this.options.inputFieldName;
val = val.split(' ').join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric
val = val
.split(' ')
.join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '')
.toLowerCase() // replace non alphanumeric
.replace(/(-)\1+/g, '-'); // replace repeated dashes
_this.cb(_this.options.fieldName, val, {}, true);
_this.input.closest('.dropdown')
_this.input
.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
});
......@@ -61,7 +71,7 @@ GitLabDropdownFilter = (function() {
ARROW_KEY_CODES = [38, 40];
HAS_VALUE_CLASS = "has-value";
HAS_VALUE_CLASS = 'has-value';
function GitLabDropdownFilter(input, options) {
var $clearButton, $inputContainer, ref, timeout;
......@@ -70,44 +80,59 @@ GitLabDropdownFilter = (function() {
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) {
$clearButton.on(
'click',
(function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('input').focus();
return _this.input
.val('')
.trigger('input')
.focus();
};
})(this));
})(this),
);
// Key events
timeout = "";
timeout = '';
this.input
.on('keydown', function (e) {
.on('keydown', function(e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', function() {
if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
.on(
'input',
function() {
if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.addClass(HAS_VALUE_CLASS);
} else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
} else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS);
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
clearTimeout(timeout);
return timeout = setTimeout(function() {
return (timeout = setTimeout(
function() {
$inputContainer.parent().addClass('is-loading');
return this.options.query(this.input.val(), function(data) {
return this.options.query(
this.input.val(),
function(data) {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
}.bind(this));
}.bind(this), 250);
}.bind(this),
);
}.bind(this),
250,
));
} else {
return this.filter(this.input.val());
}
}.bind(this));
}.bind(this),
);
}
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
......@@ -120,7 +145,7 @@ GitLabDropdownFilter = (function() {
this.options.onFilter(search_text);
}
data = this.options.data();
if ((data != null) && !this.options.filterByText) {
if (data != null && !this.options.filterByText) {
results = data;
if (search_text !== '') {
// When data is an array of objects therefore [object Array] e.g.
......@@ -130,7 +155,7 @@ GitLabDropdownFilter = (function() {
// ]
if (_.isArray(data)) {
results = fuzzaldrinPlus.filter(data, search_text, {
key: this.options.keys
key: this.options.keys,
});
} else {
// If data is grouped therefore an [object Object]. e.g.
......@@ -149,7 +174,7 @@ GitLabDropdownFilter = (function() {
for (key in data) {
group = data[key];
tmp = fuzzaldrinPlus.filter(group, search_text, {
key: this.options.keys
key: this.options.keys,
});
if (tmp.length) {
results[key] = tmp.map(function(item) {
......@@ -180,7 +205,10 @@ GitLabDropdownFilter = (function() {
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,13 +222,15 @@ GitLabDropdownRemote = (function() {
}
GitLabDropdownRemote.prototype.execute = function() {
if (typeof this.dataEndpoint === "string") {
if (typeof this.dataEndpoint === 'string') {
return this.fetchData();
} else if (typeof this.dataEndpoint === "function") {
} else if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) {
this.options.beforeSend();
}
return this.dataEndpoint("", (function(_this) {
return this.dataEndpoint(
'',
(function(_this) {
// Fetch the data by calling the data funcfion
return function(data) {
if (_this.options.success) {
......@@ -210,7 +240,8 @@ GitLabDropdownRemote = (function() {
return _this.options.beforeSend();
}
};
})(this));
})(this),
);
}
};
......@@ -220,8 +251,7 @@ GitLabDropdownRemote = (function() {
}
// Fetch the data through ajax if the data is a string
return axios.get(this.dataEndpoint)
.then(({ data }) => {
return axios.get(this.dataEndpoint).then(({ data }) => {
if (this.options.success) {
return this.options.success(data);
}
......@@ -232,21 +262,30 @@ GitLabDropdownRemote = (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;
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;
......@@ -263,15 +302,15 @@ GitLabDropdown = (function() {
this.opened = this.opened.bind(this);
this.shouldPropagate = this.shouldPropagate.bind(this);
self = this;
selector = $(this.el).data("target");
selector = $(this.el).data('target');
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
: true;
this.icon = !!this.options.icon;
this.filterInputBlur =
this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one
self = this;
// If selector was passed
......@@ -296,7 +335,13 @@ GitLabDropdown = (function() {
_this.fullData = data;
_this.parseData(_this.fullData);
_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');
}
};
......@@ -325,7 +370,7 @@ GitLabDropdown = (function() {
return function() {
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
selector = '.dropdown-page-one ' + selector;
}
return $(selector, this.instance.dropdown);
};
......@@ -341,33 +386,42 @@ GitLabDropdown = (function() {
if (_this.filterInput.val() !== '') {
selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
selector = '.dropdown-page-one ' + selector;
}
if ($(_this.el).is('input')) {
currentIndex = -1;
} else {
$(selector, _this.dropdown).first().find('a').addClass('is-focused');
$(selector, _this.dropdown)
.first()
.find('a')
.addClass('is-focused');
currentIndex = 0;
}
}
};
})(this)
})(this),
});
}
// Event listeners
this.dropdown.on("shown.bs.dropdown", this.opened);
this.dropdown.on("hidden.bs.dropdown", this.hidden);
$(this.el).on("update.label", this.updateLabel);
this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
this.dropdown.on('keyup', (function(_this) {
this.dropdown.on('shown.bs.dropdown', this.opened);
this.dropdown.on('hidden.bs.dropdown', this.hidden);
$(this.el).on('update.label', this.updateLabel);
this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
this.dropdown.on(
'keyup',
(function(_this) {
return function(e) {
// Escape key
if (e.which === 27) {
return $('.dropdown-menu-close', _this.dropdown).trigger('click');
}
};
})(this));
this.dropdown.on('blur', 'a', (function(_this) {
})(this),
);
this.dropdown.on(
'blur',
'a',
(function(_this) {
return function(e) {
var $dropdownMenu, $relatedTarget;
if (e.relatedTarget != null) {
......@@ -378,22 +432,29 @@ GitLabDropdown = (function() {
}
}
};
})(this));
if (this.dropdown.find(".dropdown-toggle-page").length) {
this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_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));
})(this),
);
}
if (this.options.selectable) {
selector = ".dropdown-content a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one .dropdown-content a";
}
this.dropdown.on("click", selector, function(e) {
selector = '.dropdown-content a';
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one .dropdown-content a';
}
this.dropdown.on(
'click',
selector,
function(e) {
var $el, selected, selectedObj, isMarking;
$el = $(e.currentTarget);
selected = self.rowClicked($el);
......@@ -414,7 +475,8 @@ GitLabDropdown = (function() {
}
$el.trigger('blur');
}.bind(this));
}.bind(this),
);
}
}
......@@ -452,10 +514,15 @@ GitLabDropdown = (function() {
html = [];
for (name in data) {
groupData = data[name];
html.push(this.renderItem({
header: name
html.push(
this.renderItem(
{
header: name,
// Add header for each group
}, name));
},
name,
),
);
this.renderData(groupData, name).map(function(item) {
return html.push(item);
});
......@@ -474,20 +541,25 @@ GitLabDropdown = (function() {
if (group == null) {
group = false;
}
return data.map((function(_this) {
return data.map(
(function(_this) {
return function(obj, index) {
return _this.renderItem(obj, group, index);
};
})(this));
})(this),
);
};
GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target;
if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') &&
if (
$target &&
!$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
!$target.data('isLink')) {
!$target.data('isLink')
) {
e.stopPropagation();
return false;
} else {
......@@ -497,9 +569,11 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.filteredFullData = function() {
return this.fullData.filter(r => typeof r === 'object'
&& !Object.prototype.hasOwnProperty.call(r, 'beforeDivider')
&& !Object.prototype.hasOwnProperty.call(r, 'header')
return this.fullData.filter(
r =>
typeof r === 'object' &&
!Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
!Object.prototype.hasOwnProperty.call(r, 'header'),
);
};
......@@ -522,11 +596,16 @@ GitLabDropdown = (function() {
// matches the correct layout
const inputValue = this.filterInput.val();
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();
if (this.remote && contentHtml === "") {
if (this.remote && contentHtml === '') {
this.remote.execute();
} else {
this.focusTextInput();
......@@ -555,11 +634,11 @@ GitLabDropdown = (function() {
var $input;
this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
$input = this.dropdown.find('.dropdown-input-field');
if (this.options.filterable) {
$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);
}
if (this.options.hidden) {
......@@ -601,7 +680,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.clearMenu = function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
if (this.dropdown.find('.dropdown-toggle-page').length) {
if (this.options.containerSelector) {
selector = this.options.containerSelector;
} else {
......@@ -619,7 +698,7 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id;
if (value) {
value = value.toString().replace(/'/g, '\\\'');
value = value.toString().replace(/'/g, "\\'");
}
}
......@@ -676,21 +755,27 @@ GitLabDropdown = (function() {
text = data.text != null ? data.text : '';
}
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
var link = document.createElement('a');
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;
} else {
link.textContent = text;
}
if (selected) {
link.className = 'is-active';
link.classList.add('is-active');
}
if (group) {
......@@ -703,17 +788,24 @@ GitLabDropdown = (function() {
return html;
};
GitLabDropdown.prototype.highlightTemplate = function(text, template) {
return `"<b>${_.escape(text)}</b>" ${template}`;
};
GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
const occurrences = fuzzaldrinPlus.match(text, term);
const { indexOf } = [];
return text.split('').map(function(character, i) {
return text
.split('')
.map(function(character, i) {
if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>";
return '<b>' + character + '</b>';
} else {
return character;
}
}).join('');
})
.join('');
};
GitLabDropdown.prototype.noResults = function() {
......@@ -748,13 +840,15 @@ GitLabDropdown = (function() {
}
field = [];
value = this.options.id
? this.options.id(selectedObject, el)
: selectedObject.id;
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
} 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)) {
......@@ -780,9 +874,12 @@ GitLabDropdown = (function() {
} else {
isMarking = true;
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) {
this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
this.dropdown
.parent()
.find("input[name='" + fieldName + "']")
.remove();
}
}
if (field && field.length && value == null) {
......@@ -823,13 +920,16 @@ GitLabDropdown = (function() {
$('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) {
$input.attr('id', this.options.inputId);
}
if (this.options.multiSelect) {
Object.keys(selectedObject).forEach((attribute) => {
Object.keys(selectedObject).forEach(attribute => {
$input.attr(`data-${attribute}`, selectedObject[attribute]);
});
}
......@@ -844,13 +944,13 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
// If we pass an option index
if (typeof index !== "undefined") {
selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
if (typeof index !== 'undefined') {
selector = SELECTABLE_CLASSES + ':eq(' + index + ') a';
} else {
selector = ".dropdown-content .is-focused";
selector = '.dropdown-content .is-focused';
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one ' + selector;
}
// simulate a click on the first link
$el = $(selector, this.dropdown);
......@@ -867,12 +967,14 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.addArrowKeyEvent = function() {
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
$input = this.dropdown.find('.dropdown-input-field');
selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one ' + selector;
}
return $('body').on('keydown', (function(_this) {
return $('body').on(
'keydown',
(function(_this) {
return function(e) {
var $listItems, PREV_INDEX, currentKeyCode;
currentKeyCode = e.which;
......@@ -885,7 +987,7 @@ GitLabDropdown = (function() {
// $input.blur()
if (currentKeyCode === 40) {
// Move down
if (currentIndex < ($listItems.length - 1)) {
if (currentIndex < $listItems.length - 1) {
currentIndex += 1;
}
} else if (currentKeyCode === 38) {
......@@ -904,7 +1006,8 @@ GitLabDropdown = (function() {
_this.selectRowAtIndex();
}
};
})(this));
})(this),
);
};
GitLabDropdown.prototype.removeArrayKeyEvent = function() {
......@@ -917,12 +1020,25 @@ GitLabDropdown = (function() {
};
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
$('.is-focused', this.dropdown).removeClass('is-focused');
// Update the class for the row at the specific 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
$dropdownContent = $listItem.closest('.dropdown-content');
dropdownScrollTop = $dropdownContent.scrollTop();
......@@ -936,15 +1052,19 @@ GitLabDropdown = (function() {
if (!index) {
// Scroll the dropdown content to the top
$dropdownContent.scrollTop(0);
} else if (index === ($listItems.length - 1)) {
} else if (index === $listItems.length - 1) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
} else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
// Scroll the dropdown content down
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
} else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
$dropdownContent.scrollTop(
listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
);
} else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
// 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() {
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) {
......
/* 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 { escape, throttle } from 'underscore';
import { s__, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_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.
......@@ -52,6 +61,7 @@ function setSearchOptions() {
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
name: s__('SearchAutocomplete|All GitLab'),
issuesPath: $dashboardOptionsDataEl.data('issuesPath'),
mrPath: $dashboardOptionsDataEl.data('mrPath'),
};
......@@ -69,8 +79,8 @@ export default class SearchAutocomplete {
this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownMenu = this.dropdown.find('.dropdown-menu');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
this.scopeInputEl = this.getElement('#scope');
this.searchInput = this.getElement('.search-input');
this.projectInputEl = this.getElement('#search_project_id');
......@@ -78,6 +88,7 @@ export default class SearchAutocomplete {
this.searchCodeInputEl = this.getElement('#search_code');
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
this.scrollFadeInitialized = false;
this.saveOriginalState();
// Only when user is logged in
......@@ -98,17 +109,18 @@ export default class SearchAutocomplete {
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
this.setScrollFade = this.setScrollFade.bind(this);
}
getElement(selector) {
return this.wrap.find(selector);
}
saveOriginalState() {
return this.originalState = this.serializeState();
return (this.originalState = this.serializeState());
}
saveTextLength() {
return this.lastTextLength = this.searchInput.val().length;
return (this.lastTextLength = this.searchInput.val().length);
}
createAutocomplete() {
......@@ -117,6 +129,7 @@ export default class SearchAutocomplete {
filterable: true,
filterRemote: true,
highlight: true,
icon: true,
enterCallback: false,
filterInput: 'input#search',
search: {
......@@ -154,13 +167,15 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true;
return axios.get(this.autocompletePath, {
return axios
.get(this.autocompletePath, {
params: {
project_id: this.projectId,
project_ref: this.projectRef,
term: term,
},
}).then((response) => {
})
.then(response => {
// Hide dropdown menu if no suggestions returns
if (!response.data.length) {
this.disableAutocomplete();
......@@ -188,6 +203,7 @@ export default class SearchAutocomplete {
}
data.push({
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
icon: this.getAvatar(suggestion),
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
......@@ -195,17 +211,41 @@ export default class SearchAutocomplete {
}
// Add option to proceed with the search
if (data.length) {
data.push('separator');
data.push({
text: `Result name contains "${term}"`,
const icon = spriteIcon('search', 's16 inline-search-icon');
let template;
if (this.projectInputEl.val()) {
template = s__('SearchAutocomplete|in this project');
}
if (this.groupInputEl.val()) {
template = s__('SearchAutocomplete|in this group');
}
data.unshift('separator');
data.unshift({
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;
}).catch(() => {
this.highlightFirstRow();
this.setScrollFade();
})
.catch(() => {
this.loadingSuggestions = false;
});
}
......@@ -236,21 +276,21 @@ export default class SearchAutocomplete {
const issueItems = [
{
text: 'Issues assigned to me',
text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_id=${userId}`,
},
{
text: "Issues I've created",
text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_id=${userId}`,
},
];
const mergeRequestItems = [
{
text: 'Merge requests assigned to me',
text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_id=${userId}`,
},
{
text: "Merge requests I've created",
text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_id=${userId}`,
},
];
......@@ -259,7 +299,7 @@ export default class SearchAutocomplete {
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
items = baseItems.concat(...issueItems, ...mergeRequestItems);
}
return items;
}
......@@ -272,8 +312,6 @@ export default class SearchAutocomplete {
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
// Location badge
_location: this.locationBadgeEl.text(),
};
}
......@@ -283,10 +321,12 @@ export default class SearchAutocomplete {
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
this.locationBadgeEl.on('click', () => this.searchInput.focus());
this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
}
enableAutocomplete() {
this.setScrollFade();
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
......@@ -308,10 +348,6 @@ export default class SearchAutocomplete {
onSearchInputKeyUp(e) {
switch (e.keyCode) {
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
if (this.lastTextLength === 1) {
this.disableAutocomplete();
......@@ -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() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i += 1) {
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() {
......@@ -411,22 +423,11 @@ export default class SearchAutocomplete {
results = [];
for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
// _location isnt a input
if (input === '_location') {
break;
}
results.push(this.getElement("#" + input).val(''));
results.push(this.getElement('#' + input).val(''));
}
return results;
}
removeLocationBadge() {
this.locationBadgeEl.hide();
this.resetSearchState();
this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete();
}
disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('disabled');
......@@ -444,23 +445,57 @@ export default class SearchAutocomplete {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
if (!this.badgePresent) {
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
this.addLocationBadge({
value: 'This project',
});
}
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
this.addLocationBadge({
value: 'This group',
});
}
}
$el.removeClass('is-active');
this.disableAutocomplete();
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 @@
*/
@mixin gitlab-theme(
$location-badge-color,
$search-and-nav-links,
$active-tab-border,
$border-and-box-shadow,
......@@ -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 {
color: rgba($search-and-nav-links, 0.8);
}
......@@ -141,10 +134,6 @@
background-color: $white-light;
}
.location-badge {
color: $gl-text-color;
}
.search-input-wrap {
.search-icon {
fill: rgba($search-and-nav-links, 0.8);
......@@ -200,7 +189,6 @@
body {
&.ui-indigo {
@include gitlab-theme(
$indigo-100,
$indigo-200,
$indigo-500,
$indigo-700,
......@@ -212,7 +200,6 @@ body {
&.ui-light-indigo {
@include gitlab-theme(
$indigo-100,
$indigo-200,
$indigo-500,
$indigo-500,
......@@ -224,7 +211,6 @@ body {
&.ui-blue {
@include gitlab-theme(
$theme-blue-100,
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
......@@ -236,7 +222,6 @@ body {
&.ui-light-blue {
@include gitlab-theme(
$theme-light-blue-100,
$theme-light-blue-200,
$theme-light-blue-500,
$theme-light-blue-500,
......@@ -248,7 +233,6 @@ body {
&.ui-green {
@include gitlab-theme(
$theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-700,
......@@ -260,7 +244,6 @@ body {
&.ui-light-green {
@include gitlab-theme(
$theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-500,
......@@ -272,7 +255,6 @@ body {
&.ui-red {
@include gitlab-theme(
$theme-red-100,
$theme-red-200,
$theme-red-500,
$theme-red-700,
......@@ -284,7 +266,6 @@ body {
&.ui-light-red {
@include gitlab-theme(
$theme-light-red-100,
$theme-light-red-200,
$theme-light-red-500,
$theme-light-red-500,
......@@ -296,7 +277,6 @@ body {
&.ui-dark {
@include gitlab-theme(
$theme-gray-100,
$theme-gray-200,
$theme-gray-500,
$theme-gray-700,
......@@ -308,7 +288,6 @@ body {
&.ui-light {
@include gitlab-theme(
$theme-gray-900,
$theme-gray-700,
$theme-gray-800,
$theme-gray-700,
......@@ -357,10 +336,6 @@ body {
&:hover {
background-color: $white-light;
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 {
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 {
......
......@@ -467,7 +467,8 @@ $award-emoji-positive-add-lines: #bb9c13;
*/
$search-input-border-color: rgba($blue-400, 0.8);
$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-icon-color: #e7e9ed;
......
$search-dropdown-max-height: 400px;
$search-avatar-size: 16px;
.search-results {
.search-result-row {
border-bottom: 1px solid $border-color;
......@@ -24,8 +27,9 @@
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
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%);
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%);
}
.search {
......@@ -40,24 +44,15 @@ input[type="checkbox"]:hover {
height: 32px;
border: 0;
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 {
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 {
border: 0;
font-size: 14px;
......@@ -104,17 +99,28 @@ input[type="checkbox"]:hover {
}
.dropdown-header {
text-transform: uppercase;
font-size: 11px;
// Necessary because glDropdown doesn't support a second style of headers
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
.dropdown-menu {
left: -5px;
max-height: $search-dropdown-max-height;
overflow: auto;
@include media-breakpoint-up(xl) {
width: $search-input-active-width;
}
}
.dropdown-content {
max-height: none;
max-height: $search-dropdown-max-height - 18px;
}
}
......@@ -124,6 +130,10 @@ input[type="checkbox"]:hover {
border-color: $dropdown-input-focus-border;
box-shadow: none;
@include media-breakpoint-up(xl) {
width: $search-input-active-width;
}
.search-input-wrap {
.search-icon,
.clear-icon {
......@@ -141,12 +151,6 @@ input[type="checkbox"]:hover {
color: $gl-text-color-tertiary;
}
}
.location-badge {
transition: all $default-transition-duration;
background-color: $nav-badge-bg;
border-color: $border-color;
}
}
&.has-value {
......@@ -160,10 +164,24 @@ input[type="checkbox"]:hover {
}
}
&.has-location-badge {
.search-input-wrap {
width: 68%;
.inline-search-icon {
position: relative;
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
ref = @ref || @project.repository.root_ref
[
{ category: "Current Project", label: "Files", url: project_tree_path(@project, ref) },
{ category: "Current Project", label: "Commits", url: project_commits_path(@project, ref) },
{ category: "Current Project", label: "Network", url: project_network_path(@project, ref) },
{ category: "Current Project", label: "Graph", url: project_graph_path(@project, ref) },
{ category: "Current Project", label: "Issues", url: project_issues_path(@project) },
{ category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "Current Project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "Current Project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "Current Project", label: "Members", url: project_project_members_path(@project) },
{ category: "Current Project", label: "Wiki", url: project_wikis_path(@project) }
{ category: "In this project", label: "Files", url: project_tree_path(@project, ref) },
{ category: "In this project", label: "Commits", url: project_commits_path(@project, ref) },
{ category: "In this project", label: "Network", url: project_network_path(@project, ref) },
{ category: "In this project", label: "Graph", url: project_graph_path(@project, ref) },
{ category: "In this project", label: "Issues", url: project_issues_path(@project) },
{ category: "In this project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "In this project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "In this project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "In this project", label: "Members", url: project_project_members_path(@project) },
{ category: "In this project", label: "Wiki", url: project_wikis_path(@project) }
]
else
[]
......
......@@ -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)) }
- 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? }
.search.search-form{ class: "#{'has-location-badge' if label.present?}" }
.search.search-form
= form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
- if label.present?
.location-badge= label
.search-input-wrap
.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',
spellcheck: false,
tabindex: '1',
autocomplete: 'off',
data: { issues_path: issues_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' } }
.dropdown-menu.dropdown-select
= 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 ""
msgid "Search milestones"
msgstr ""
msgid "Search or jump to…"
msgstr ""
msgid "Search project"
msgstr ""
msgid "Search users"
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"
msgstr ""
......
......@@ -62,10 +62,6 @@ describe 'User uses header search field' do
end
end
it 'contains location badge' do
expect(page).to have_selector('.has-location-badge')
end
context 'when clicking the search field', :js do
before do
page.find('#search').click
......
.search.search-form.has-location-badge
%form.navbar-form
.search.search-form
%form.form-inline
.search-input-container
%div.location-badge
This project
.search-input-wrap
.dropdown
%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