Commit c9c75dec authored by Tim Zallmann's avatar Tim Zallmann Committed by Kushal Pandya

On-demand loading of select2

Fix for loading select2 on demand

Fix of general select2 setup + linting fixes

Typo in exclude for stylelint
Added lazy CSS Loading for all Select2 instances

Fixed Spec around Select 2 + no-nesting  rules
parent ce9924ad
import $ from 'jquery';
import { loadCSSFile } from '../lib/utils/css_utils';
export default () => {
if ($('select.select2').length) {
const $select2Elements = $('select.select2');
if ($select2Elements.length) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
$('select.select2').select2({
width: 'resolve',
minimumResultsForSearch: 10,
dropdownAutoWidth: true,
});
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$select2Elements.select2({
width: 'resolve',
minimumResultsForSearch: 10,
dropdownAutoWidth: true,
});
// Close select2 on escape
$('.js-select2').on('select2-close', () => {
setTimeout(() => {
$('.select2-container-active').removeClass('select2-container-active');
$(':focus').blur();
}, 1);
});
// Close select2 on escape
$('.js-select2').on('select2-close', () => {
requestAnimationFrame(() => {
$('.select2-container-active').removeClass('select2-container-active');
$(':focus').blur();
});
});
})
.catch(() => {});
})
.catch(() => {});
}
......
......@@ -4,97 +4,107 @@ import axios from './lib/utils/axios_utils';
import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale';
import { loadCSSFile } from './lib/utils/css_utils';
const fetchGroups = params => {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then(res => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
params.success({
results,
pagination: {
more,
},
});
})
.catch(params.error);
};
const groupsSelect = () => {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || [];
const parentGroupID = $select.data('parentId');
const groupsPath = parentGroupID
? Api.subgroupsPath.replace(':id', parentGroupID)
: Api.groupsPath;
loadCSSFile(gon.select2_css_path)
.then(() => {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$select.select2({
placeholder: __('Search for a group'),
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then(res => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || [];
const parentGroupID = $select.data('parentId');
const groupsPath = parentGroupID
? Api.subgroupsPath.replace(':id', parentGroupID)
: Api.groupsPath;
params.success({
results,
pagination: {
more,
},
});
})
.catch(params.error);
},
data(search, page) {
return {
search,
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
};
},
results(data, page) {
if (data.length) return { results: [] };
$select.select2({
placeholder: __('Search for a group'),
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
fetchGroups(params);
},
data(search, page) {
return {
search,
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
};
},
results(data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
return {
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${escape(
object.full_name,
)}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return escape(object.full_name);
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
return {
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${escape(
object.full_name,
)}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return escape(object.full_name);
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
$select.on('select2-loaded', () => {
const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
$select.on('select2-loaded', () => {
const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
})
.catch(() => {});
};
export default () => {
......
import $ from 'jquery';
import { loadCSSFile } from '../lib/utils/css_utils';
let instanceCount = 0;
......@@ -13,10 +14,15 @@ class AutoWidthDropdownSelect {
const { dropdownClass } = this;
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
this.$selectElement.select2({
dropdownCssClass: dropdownClass,
...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
});
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
this.$selectElement.select2({
dropdownCssClass: dropdownClass,
...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
});
})
.catch(() => {});
})
.catch(() => {});
......
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import UsersSelect from './users_select';
import { loadCSSFile } from './lib/utils/css_utils';
export default class IssuableContext {
constructor(currentUser) {
......@@ -10,10 +11,15 @@ export default class IssuableContext {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true,
});
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true,
});
})
.catch(() => {});
})
.catch(() => {});
......
......@@ -7,6 +7,7 @@ import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { queryToObject, objectToQuery } from './lib/utils/url_utility';
import { loadCSSFile } from './lib/utils/css_utils';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
......@@ -184,36 +185,41 @@ export default class IssuableForm {
initTargetBranchDropdown() {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
this.$targetBranchSelect.select2({
...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
ajax: {
url: this.$targetBranchSelect.data('endpoint'),
dataType: 'JSON',
quietMillis: 250,
data(search) {
return {
search,
};
},
results(data) {
return {
// `data` keys are translated so we can't just access them with a string based key
results: data[Object.keys(data)[0]].map(name => ({
id: name,
text: name,
})),
};
},
},
initSelection(el, callback) {
const val = el.val();
callback({
id: val,
text: val,
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
this.$targetBranchSelect.select2({
...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
ajax: {
url: this.$targetBranchSelect.data('endpoint'),
dataType: 'JSON',
quietMillis: 250,
data(search) {
return {
search,
};
},
results(data) {
return {
// `data` keys are translated so we can't just access them with a string based key
results: data[Object.keys(data)[0]].map(name => ({
id: name,
text: name,
})),
};
},
},
initSelection(el, callback) {
const val = el.val();
callback({
id: val,
text: val,
});
},
});
},
});
})
.catch(() => {});
})
.catch(() => {});
}
......
......@@ -4,110 +4,116 @@ import $ from 'jquery';
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
import { s__ } from './locale';
import { loadCSSFile } from './lib/utils/css_utils';
const projectSelect = () => {
$('.ajax-project-select').each(function(i, select) {
let placeholder;
const simpleFilter = $(select).data('simpleFilter') || false;
const isInstantiated = $(select).data('select2');
this.groupId = $(select).data('groupId');
this.userId = $(select).data('userId');
this.includeGroups = $(select).data('includeGroups');
this.allProjects = $(select).data('allProjects') || false;
this.orderBy = $(select).data('orderBy') || 'id';
this.withIssuesEnabled = $(select).data('withIssuesEnabled');
this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
this.withShared =
$(select).data('withShared') === undefined ? true : $(select).data('withShared');
this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
this.allowClear = $(select).data('allowClear') || false;
loadCSSFile(gon.select2_css_path)
.then(() => {
$('.ajax-project-select').each(function(i, select) {
let placeholder;
const simpleFilter = $(select).data('simpleFilter') || false;
const isInstantiated = $(select).data('select2');
this.groupId = $(select).data('groupId');
this.userId = $(select).data('userId');
this.includeGroups = $(select).data('includeGroups');
this.allProjects = $(select).data('allProjects') || false;
this.orderBy = $(select).data('orderBy') || 'id';
this.withIssuesEnabled = $(select).data('withIssuesEnabled');
this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
this.withShared =
$(select).data('withShared') === undefined ? true : $(select).data('withShared');
this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
this.allowClear = $(select).data('allowClear') || false;
placeholder = s__('ProjectSelect|Search for project');
if (this.includeGroups) {
placeholder += s__('ProjectSelect| or group');
}
$(select).select2({
placeholder,
minimumInputLength: 0,
query: query => {
let projectsCallback;
const finalCallback = function(projects) {
const data = {
results: projects,
};
return query.callback(data);
};
placeholder = s__('ProjectSelect|Search for project');
if (this.includeGroups) {
projectsCallback = function(projects) {
const groupsCallback = function(groups) {
const data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(query.term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (this.groupId) {
return Api.groupProjects(
this.groupId,
query.term,
{
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared,
include_subgroups: this.includeProjectsInSubgroups,
order_by: 'similarity',
},
projectsCallback,
);
} else if (this.userId) {
return Api.userProjects(
this.userId,
query.term,
{
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared,
include_subgroups: this.includeProjectsInSubgroups,
},
projectsCallback,
);
placeholder += s__('ProjectSelect| or group');
}
return Api.projects(
query.term,
{
order_by: this.orderBy,
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
membership: !this.allProjects,
$(select).select2({
placeholder,
minimumInputLength: 0,
query: query => {
let projectsCallback;
const finalCallback = function(projects) {
const data = {
results: projects,
};
return query.callback(data);
};
if (this.includeGroups) {
projectsCallback = function(projects) {
const groupsCallback = function(groups) {
const data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(query.term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (this.groupId) {
return Api.groupProjects(
this.groupId,
query.term,
{
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared,
include_subgroups: this.includeProjectsInSubgroups,
order_by: 'similarity',
},
projectsCallback,
);
} else if (this.userId) {
return Api.userProjects(
this.userId,
query.term,
{
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared,
include_subgroups: this.includeProjectsInSubgroups,
},
projectsCallback,
);
}
return Api.projects(
query.term,
{
order_by: this.orderBy,
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
membership: !this.allProjects,
},
projectsCallback,
);
},
id(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
text(project) {
return project.name_with_namespace || project.name;
},
projectsCallback,
);
},
id(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
text(project) {
return project.name_with_namespace || project.name;
},
initSelection(el, callback) {
return Api.project(el.val()).then(({ data }) => callback(data));
},
initSelection(el, callback) {
// eslint-disable-next-line promise/no-nesting
return Api.project(el.val()).then(({ data }) => callback(data));
},
allowClear: this.allowClear,
allowClear: this.allowClear,
dropdownCssClass: 'ajax-project-dropdown',
});
if (isInstantiated || simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
dropdownCssClass: 'ajax-project-dropdown',
});
if (isInstantiated || simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
})
.catch(() => {});
};
export default () => {
......
import $ from 'jquery';
import AccessorUtilities from './lib/utils/accessor';
import { loadCSSFile } from './lib/utils/css_utils';
export default class ProjectSelectComboButton {
constructor(select) {
......@@ -46,9 +47,14 @@ export default class ProjectSelectComboButton {
openDropdown(event) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
$(event.currentTarget)
.siblings('.project-item-select')
.select2('open');
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$(event.currentTarget)
.siblings('.project-item-select')
.select2('open');
})
.catch(() => {});
})
.catch(() => {});
}
......
......@@ -15,6 +15,7 @@ import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { fixTitle, dispose } from '~/tooltips';
import { loadCSSFile } from '../lib/utils/css_utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
......@@ -592,92 +593,97 @@ function UsersSelect(currentUser, els, options = {}) {
if ($('.ajax-users-select').length) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
$('.ajax-users-select').each((i, select) => {
const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
options.skipLdap = $(select).hasClass('skip_ldap');
const showNullUser = $(select).data('nullUser');
const showAnyUser = $(select).data('anyUser');
const showEmailUser = $(select).data('emailUser');
const firstUser = $(select).data('firstUser');
return $(select).select2({
placeholder: __('Search for a user'),
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query(query) {
return userSelect.users(query.term, options, users => {
let name;
const data = {
results: users,
};
if (query.term.length === 0) {
if (firstUser) {
// Move current user to the front of the list
const ref = data.results;
for (let index = 0, len = ref.length; index < len; index += 1) {
const obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
data.results.unshift(obj);
break;
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$('.ajax-users-select').each((i, select) => {
const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
options.skipLdap = $(select).hasClass('skip_ldap');
const showNullUser = $(select).data('nullUser');
const showAnyUser = $(select).data('anyUser');
const showEmailUser = $(select).data('emailUser');
const firstUser = $(select).data('firstUser');
return $(select).select2({
placeholder: __('Search for a user'),
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query(query) {
return userSelect.users(query.term, options, users => {
let name;
const data = {
results: users,
};
if (query.term.length === 0) {
if (firstUser) {
// Move current user to the front of the list
const ref = data.results;
for (let index = 0, len = ref.length; index < len; index += 1) {
const obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
data.results.unshift(obj);
break;
}
}
}
if (showNullUser) {
const nullUser = {
name: s__('UsersSelect|Unassigned'),
id: 0,
};
data.results.unshift(nullUser);
}
if (showAnyUser) {
name = showAnyUser;
if (name === true) {
name = s__('UsersSelect|Any User');
}
const anyUser = {
name,
id: null,
};
data.results.unshift(anyUser);
}
}
}
if (showNullUser) {
const nullUser = {
name: s__('UsersSelect|Unassigned'),
id: 0,
};
data.results.unshift(nullUser);
}
if (showAnyUser) {
name = showAnyUser;
if (name === true) {
name = s__('UsersSelect|Any User');
if (
showEmailUser &&
data.results.length === 0 &&
query.term.match(/^[^@]+@[^@]+$/)
) {
const trimmed = query.term.trim();
const emailUser = {
name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
username: trimmed,
id: trimmed,
invite: true,
};
data.results.unshift(emailUser);
}
const anyUser = {
name,
id: null,
};
data.results.unshift(anyUser);
}
}
if (
showEmailUser &&
data.results.length === 0 &&
query.term.match(/^[^@]+@[^@]+$/)
) {
const trimmed = query.term.trim();
const emailUser = {
name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
username: trimmed,
id: trimmed,
invite: true,
};
data.results.unshift(emailUser);
}
return query.callback(data);
return query.callback(data);
});
},
initSelection() {
const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return userSelect.initSelection.apply(userSelect, args);
},
formatResult() {
const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return userSelect.formatResult.apply(userSelect, args);
},
formatSelection() {
const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return userSelect.formatSelection.apply(userSelect, args);
},
dropdownCssClass: 'ajax-users-dropdown',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
},
initSelection() {
const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return userSelect.initSelection.apply(userSelect, args);
},
formatResult() {
const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return userSelect.formatResult.apply(userSelect, args);
},
formatSelection() {
const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
return userSelect.formatSelection.apply(userSelect, args);
},
dropdownCssClass: 'ajax-users-dropdown',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
});
});
})
.catch(() => {});
})
.catch(() => {});
}
......
......@@ -5,7 +5,6 @@
// directory.
@import '@gitlab/at.js/dist/css/jquery.atwho';
@import 'dropzone/dist/basic';
@import 'select2';
// GitLab UI framework
@import 'framework';
......
......@@ -135,7 +135,6 @@ hr {
text-overflow: ellipsis;
white-space: nowrap;
> div:not(.block):not(.select2-display-none),
.str-truncated {
display: inline;
}
......
......@@ -133,11 +133,6 @@ label {
}
.input-group {
.select2-container {
display: table-cell;
max-width: 180px;
}
.input-group-prepend,
.input-group-append {
background-color: $input-group-addon-bg;
......
/** Select2 selectbox style override **/
.select2-container {
width: 100% !important;
&.input-md,
&.input-lg {
display: block;
}
}
.select2-container,
.select2-container.select2-drop-above {
.select2-choice {
background: $white;
color: $gl-text-color;
border-color: $input-border;
height: 34px;
padding: $gl-vert-padding $gl-input-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
border-radius: $border-radius-base;
.select2-arrow {
background-image: none;
background-color: transparent;
border: 0;
padding-top: 12px;
padding-right: 20px;
font-size: 10px;
b {
display: none;
}
&::after {
content: '\f078';
position: absolute;
z-index: 1;
text-align: center;
pointer-events: none;
box-sizing: border-box;
color: $gray-darkest;
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
.select2-chosen {
margin-right: 15px;
}
&:hover {
border-color: $gray-darkest;
color: $gl-text-color;
}
}
// Essentially we’re doing @include form-control-focus here (from
// bootstrap/scss/mixins/_forms.scss), except that the bootstrap mixin adds a
// `&:focus` selector and we’re never actually focusing the .select2-choice
// link nor the .select2-container, the Select2 library focuses an off-screen
// .select2-focusser element instead.
&.select2-container-active:not(.select2-dropdown-open) {
.select2-choice {
color: $input-focus-color;
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
outline: 0;
}
// Reusable focus “glow” box-shadow
@mixin form-control-focus-glow {
@if $enable-shadows {
box-shadow: $input-box-shadow, $input-focus-box-shadow;
} @else {
box-shadow: $input-focus-box-shadow;
}
}
// Apply the focus “glow” shadow to the .select2-container if it also has
// the .block-truncated class as that applies an overflow: hidden, thereby
// hiding the glow of the nested .select2-choice element.
&.block-truncated {
@include form-control-focus-glow;
}
// Apply the glow directly to the .select2-choice link if we’re not
// block-truncating the container.
&:not(.block-truncated) .select2-choice {
@include form-control-focus-glow;
}
}
&.is-invalid {
~ .invalid-feedback {
display: block;
}
.select2-choices,
.select2-choice {
border-color: $red-500;
}
}
}
.select2-drop,
.select2-drop.select2-drop-above {
background: $white;
box-shadow: 0 2px 4px $dropdown-shadow-color;
border-radius: $border-radius-base;
border: 1px solid $border-color;
min-width: 175px;
color: $gl-text-color;
z-index: 999;
.modal-open & {
z-index: $zindex-modal + 200;
}
}
.select2-drop-mask {
z-index: 998;
.modal-open & {
z-index: $zindex-modal + 100;
}
}
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $border-color;
margin-top: -6px;
}
.select2-container-active {
.select2-choice,
.select2-choices {
box-shadow: none;
}
}
.select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice {
border-color: $gray-darkest;
outline: 0;
}
}
.select2-container-multi {
.select2-choices {
border-radius: $border-radius-default;
border-color: $input-border;
background: none;
.select2-search-field input {
padding: 5px $gl-input-padding;
height: auto;
font-family: inherit;
font-size: inherit;
}
.select2-search-choice {
margin: 5px 0 0 8px;
box-shadow: none;
border-color: $input-border;
color: $gl-text-color;
line-height: 15px;
background-color: $gray-light;
background-image: none;
padding: 3px 18px 3px 5px;
.select2-search-choice-close {
top: 5px;
left: initial;
right: 3px;
}
&.select2-search-choice-focus {
border-color: $gl-text-color;
}
}
}
}
.select2-drop-active {
margin-top: $dropdown-vertical-offset;
font-size: 14px;
.select2-results {
max-height: 350px;
}
}
.select2-search {
padding: $grid-size;
.select2-drop-auto-width & {
padding: $grid-size;
}
input {
padding: $grid-size;
background: transparent image-url('select2.png');
color: $gl-text-color;
background-clip: content-box;
background-origin: content-box;
background-repeat: no-repeat;
background-position: right 0 bottom 0 !important;
border: 1px solid $input-border;
border-radius: $border-radius-default;
line-height: 16px;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
border-color: $blue-300;
}
&.select2-active {
background-color: $white;
background-image: image-url('select2-spinner.gif') !important;
background-origin: content-box;
background-repeat: no-repeat;
background-position: right 6px center !important;
background-size: 16px 16px !important;
}
}
+ .select2-results {
padding-top: 0;
}
}
.select2-results {
margin: 0;
padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $gray-darker;
}
}
.select2-result {
padding: 0 1px;
}
li.select2-result-with-children > .select2-result-label {
font-weight: $gl-font-weight-bold;
color: $gl-text-color;
}
}
.ajax-users-select {
width: 400px;
......@@ -282,14 +10,6 @@
}
}
.select2-highlighted {
.group-result {
.group-path {
color: $gray-700;
}
}
}
.group-result {
.group-image {
float: left;
......@@ -345,11 +65,3 @@
.ajax-users-dropdown {
min-width: 250px !important;
}
.select2-result-selectable,
.select2-result-unselectable {
.select2-match {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
}
......@@ -74,10 +74,6 @@
justify-content: flex-end;
}
.select2 {
float: right;
}
.encoding-selector,
.soft-wrap-toggle {
display: inline-block;
......
......@@ -17,14 +17,6 @@
max-width: 300px;
}
.import-namespace-select {
> .select2-choice {
border-radius: $border-radius-default 0 0 $border-radius-default;
position: relative;
left: 1px;
}
}
.import-slash-divider {
background-color: $gray-lightest;
border: 1px solid $border-color;
......
......@@ -199,10 +199,6 @@
border: 0;
}
.select2-container span {
margin-top: 0;
}
&.assignee {
.author-link {
display: block;
......
......@@ -92,6 +92,11 @@ ul.related-merge-requests > li {
}
}
.issues-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
}
.issues-nav-controls,
.new-branch-col {
font-size: 0;
......
......@@ -10,12 +10,6 @@
}
.input-group {
.select2-container {
display: unset;
max-width: unset;
flex-grow: 1;
}
> div {
&:last-child {
padding-right: 0;
......@@ -52,7 +46,6 @@
flex-grow: 1;
}
+ .select2 a,
+ .btn-default {
border-radius: 0 $border-radius-base $border-radius-base 0;
}
......@@ -258,10 +251,6 @@
color: $gray-700;
}
.transfer-project .select2-container {
min-width: 200px;
}
.deploy-key {
// Ensure that the fingerprint does not overflow on small screens
.fingerprint {
......@@ -1057,11 +1046,6 @@ pre.light-well {
margin-bottom: 0;
}
}
.select2-choice {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.project-home-empty {
......
<script>
import $ from 'jquery';
import 'select2/select2';
import { escape, debounce } from 'lodash';
import Api from 'ee/api';
import { __ } from '~/locale';
import { TYPE_USER, TYPE_GROUP } from '../constants';
import { renderAvatar } from '~/helpers/avatar_helper';
import { loadCSSFile } from '~/lib/utils/css_utils';
function addType(type) {
return items => items.map(obj => Object.assign(obj, { type }));
......@@ -97,18 +97,30 @@ export default {
},
},
mounted() {
$(this.$refs.input)
.select2({
placeholder: __('Search users or groups'),
minimumInputLength: 0,
multiple: true,
closeOnSelect: false,
formatResult,
formatSelection,
query: debounce(({ term, callback }) => this.fetchGroupsAndUsers(term).then(callback), 250),
id: ({ type, id }) => `${type}${id}`,
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$(this.$refs.input)
.select2({
placeholder: __('Search users or groups'),
minimumInputLength: 0,
multiple: true,
closeOnSelect: false,
formatResult,
formatSelection,
query: debounce(({ term, callback }) => {
// eslint-disable-next-line promise/no-nesting
return this.fetchGroupsAndUsers(term).then(callback);
}, 250),
id: ({ type, id }) => `${type}${id}`,
})
.on('change', e => this.onChange(e));
})
.catch(() => {});
})
.on('change', e => this.onChange(e));
.catch(() => {});
},
beforeDestroy() {
$(this.$refs.input).select2('destroy');
......
......@@ -3,6 +3,7 @@
import $ from 'jquery';
import Api from 'ee/api';
import { __ } from '~/locale';
import { loadCSSFile } from '~/lib/utils/css_utils';
export default function initLDAPGroupsSelect() {
const ldapGroupResult = function(group) {
......@@ -13,38 +14,43 @@ export default function initLDAPGroupsSelect() {
};
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
$('.ajax-ldap-groups-select').each((i, select) => {
$(select).select2({
id(group) {
return group.cn;
},
placeholder: __('Search for a LDAP group'),
minimumInputLength: 1,
query(query) {
const provider = $('#ldap_group_link_provider').val();
return Api.ldapGroups(query.term, provider, groups => {
const data = {
results: groups,
};
return query.callback(data);
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$('.ajax-ldap-groups-select').each((i, select) => {
$(select).select2({
id(group) {
return group.cn;
},
placeholder: __('Search for a LDAP group'),
minimumInputLength: 1,
query(query) {
const provider = $('#ldap_group_link_provider').val();
return Api.ldapGroups(query.term, provider, groups => {
const data = {
results: groups,
};
return query.callback(data);
});
},
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return callback({
cn: id,
});
}
},
formatResult: ldapGroupResult,
formatSelection: groupFormatSelection,
dropdownCssClass: 'ajax-groups-dropdown',
formatNoMatches() {
return __('Match not found; try refining your search query.');
},
});
},
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return callback({
cn: id,
});
}
},
formatResult: ldapGroupResult,
formatSelection: groupFormatSelection,
dropdownCssClass: 'ajax-groups-dropdown',
formatNoMatches() {
return __('Match not found; try refining your search query.');
},
});
});
});
})
.catch(() => {});
})
.catch(() => {});
......
import '~/pages/admin/application_settings/index';
import 'select2/select2';
import $ from 'jquery';
import groupsSelect from '~/groups_select';
import { s__ } from '~/locale';
import Api from '~/api';
import { loadCSSFile } from '~/lib/utils/css_utils';
const onLimitCheckboxChange = (checked, $limitByNamespaces, $limitByProjects) => {
$limitByNamespaces.find('.select2').select2('data', null);
......@@ -53,22 +53,31 @@ $container
),
);
$container
.find('.js-elasticsearch-namespaces')
.select2(
getDropdownConfig(
s__('Elastic|None. Select namespaces to index.'),
Api.namespacesPath,
'full_path',
),
);
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$container
.find('.js-elasticsearch-namespaces')
.select2(
getDropdownConfig(
s__('Elastic|None. Select namespaces to index.'),
Api.namespacesPath,
'full_path',
),
);
$container
.find('.js-elasticsearch-projects')
.select2(
getDropdownConfig(
s__('Elastic|None. Select projects to index.'),
Api.projectsPath,
'name_with_namespace',
),
);
$container
.find('.js-elasticsearch-projects')
.select2(
getDropdownConfig(
s__('Elastic|None. Select projects to index.'),
Api.projectsPath,
'name_with_namespace',
),
);
})
.catch(() => {});
})
.catch(() => {});
import 'select2/select2';
import $ from 'jquery';
import { s__ } from '~/locale';
import PersistentUserCallout from '~/persistent_user_callout';
import { loadCSSFile } from '~/lib/utils/css_utils';
const onLimitCheckboxChange = (checked, $limitByNamespaces, $limitByProjects) => {
$limitByNamespaces.find('.select2').select2('data', null);
......@@ -52,20 +52,29 @@ $container
),
);
$container
.find('.js-elasticsearch-namespaces')
.select2(
getDropdownConfig(
s__('Elastic|None. Select namespaces to index.'),
'/-/autocomplete/namespace_routes.json',
),
);
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$container
.find('.js-elasticsearch-namespaces')
.select2(
getDropdownConfig(
s__('Elastic|None. Select namespaces to index.'),
'/-/autocomplete/namespace_routes.json',
),
);
$container
.find('.js-elasticsearch-projects')
.select2(
getDropdownConfig(
s__('Elastic|None. Select projects to index.'),
'/-/autocomplete/project_routes.json',
),
);
$container
.find('.js-elasticsearch-projects')
.select2(
getDropdownConfig(
s__('Elastic|None. Select projects to index.'),
'/-/autocomplete/project_routes.json',
),
);
})
.catch(() => {});
})
.catch(() => {});
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import Api from '~/api';
import { sprintf, __ } from '~/locale';
import { sanitizeItem } from '~/frequent_items/utils';
import { loadCSSFile } from '~/lib/utils/css_utils';
const formatResult = selectedItem => {
if (selectedItem.path_with_namespace) {
......@@ -23,48 +24,56 @@ const formatSelection = selectedItem => {
return __('All groups and projects');
};
const QueryAdmin = query => {
const groupsFetch = Api.groups(query.term, {});
const projectsFetch = Api.projects(query.term, {
order_by: 'id',
membership: false,
});
return Promise.all([projectsFetch, groupsFetch]).then(([projects, groups]) => {
const all = {
id: 'all',
};
const data = [all].concat(groups, projects.data).map(sanitizeItem);
return query.callback({
results: data,
});
});
};
const AdminEmailSelect = () => {
$('.ajax-admin-email-select').each((i, select) =>
$(select).select2({
placeholder: __('Select group or project'),
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query(query) {
const groupsFetch = Api.groups(query.term, {});
const projectsFetch = Api.projects(query.term, {
order_by: 'id',
membership: false,
});
return Promise.all([projectsFetch, groupsFetch]).then(([projects, groups]) => {
const all = {
id: 'all',
};
const data = [all].concat(groups, projects.data).map(sanitizeItem);
return query.callback({
results: data,
});
});
},
id(object) {
if (object.path_with_namespace) {
return `project-${object.id}`;
} else if (object.path) {
return `group-${object.id}`;
}
return 'all';
},
formatResult(...args) {
return formatResult(...args);
},
formatSelection(...args) {
return formatSelection(...args);
},
dropdownCssClass: 'ajax-admin-email-dropdown',
escapeMarkup(m) {
return m;
},
}),
);
loadCSSFile(gon.select2_css_path)
.then(() => {
$('.ajax-admin-email-select').each((i, select) =>
$(select).select2({
placeholder: __('Select group or project'),
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query(query) {
QueryAdmin(query);
},
id(object) {
if (object.path_with_namespace) {
return `project-${object.id}`;
} else if (object.path) {
return `group-${object.id}`;
}
return 'all';
},
formatResult(...args) {
return formatResult(...args);
},
formatSelection(...args) {
return formatSelection(...args);
},
dropdownCssClass: 'ajax-admin-email-dropdown',
escapeMarkup(m) {
return m;
},
}),
);
})
.catch(() => {});
};
export default () =>
......
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
import MirrorRepos from '~/mirrors/mirror_repos';
import { loadCSSFile } from '~/lib/utils/css_utils';
export default class EEMirrorRepos extends MirrorRepos {
constructor(...args) {
......@@ -78,10 +79,15 @@ export default class EEMirrorRepos extends MirrorRepos {
initSelect2() {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
$('.js-mirror-user', this.$form).select2({
width: 'resolve',
dropdownAutoWidth: true,
});
// eslint-disable-next-line promise/no-nesting
loadCSSFile(gon.select2_css_path)
.then(() => {
$('.js-mirror-user', this.$form).select2({
width: 'resolve',
dropdownAutoWidth: true,
});
})
.catch(() => {});
})
.catch(() => {});
}
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import 'select2/select2';
import Api from 'ee/api';
import ApproversSelect from 'ee/approvals/components/approvers_select.vue';
import { TYPE_USER, TYPE_GROUP } from 'ee/approvals/constants';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
const TEST_PROJECT_ID = '17';
const TEST_GROUP_AVATAR = `${TEST_HOST}/group-avatar.png`;
......@@ -51,19 +53,21 @@ describe('Approvals ApproversSelect', () => {
let wrapper;
let $input;
const factory = (options = {}) => {
const factory = async (options = {}) => {
const propsData = {
projectId: TEST_PROJECT_ID,
...options.propsData,
};
wrapper = shallowMount(ApproversSelect, {
wrapper = await shallowMount(ApproversSelect, {
...options,
propsData,
localVue,
attachToDocument: true,
});
await waitForPromises();
$input = $(wrapper.vm.$refs.input);
};
const search = (term = '') => {
......@@ -80,16 +84,16 @@ describe('Approvals ApproversSelect', () => {
wrapper.destroy();
});
it('renders select2 input', () => {
it('renders select2 input', async () => {
expect(select2Container()).toBe(null);
factory();
await factory();
expect(select2Container()).not.toBe(null);
});
it('queries and displays groups and users', done => {
factory();
it('queries and displays groups and users', async done => {
await factory();
const expected = TEST_GROUPS.concat(TEST_USERS)
.map(({ id, ...obj }) => obj)
......@@ -110,8 +114,8 @@ describe('Approvals ApproversSelect', () => {
describe('with search term', () => {
const term = 'lorem';
beforeEach(done => {
factory();
beforeEach(async done => {
await factory();
waitForEvent($input, 'select2-loaded')
.then(jest.runOnlyPendingTimers)
......@@ -136,8 +140,8 @@ describe('Approvals ApproversSelect', () => {
const skipGroupIds = [7, 8];
const skipUserIds = [9, 10];
beforeEach(done => {
factory({
beforeEach(async done => {
await factory({
propsData: {
skipGroupIds,
skipUserIds,
......@@ -166,8 +170,8 @@ describe('Approvals ApproversSelect', () => {
});
});
it('emits input when data changes', done => {
factory();
it('emits input when data changes', async done => {
await factory();
const expectedFinal = [
{ ...TEST_USERS[0], type: TYPE_USER },
......
This diff is collapsed.
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