2018-03-17 18:26:18 +05:30
|
|
|
import _ from 'underscore';
|
2017-09-10 17:25:29 +05:30
|
|
|
import AjaxCache from '../lib/utils/ajax_cache';
|
2018-03-17 18:26:18 +05:30
|
|
|
import Flash from '../flash';
|
2017-08-17 22:00:37 +05:30
|
|
|
import FilteredSearchContainer from './container';
|
2017-09-10 17:25:29 +05:30
|
|
|
import UsersCache from '../lib/utils/users_cache';
|
2017-08-17 22:00:37 +05:30
|
|
|
|
|
|
|
class FilteredSearchVisualTokens {
|
|
|
|
static getLastVisualTokenBeforeInput() {
|
|
|
|
const inputLi = FilteredSearchContainer.container.querySelector('.input-token');
|
|
|
|
const lastVisualToken = inputLi && inputLi.previousElementSibling;
|
|
|
|
|
|
|
|
return {
|
|
|
|
lastVisualToken,
|
|
|
|
isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static unselectTokens() {
|
|
|
|
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
|
|
|
|
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
|
|
|
|
}
|
|
|
|
|
|
|
|
static selectToken(tokenButton, forceSelection = false) {
|
|
|
|
const selected = tokenButton.classList.contains('selected');
|
|
|
|
FilteredSearchVisualTokens.unselectTokens();
|
|
|
|
|
|
|
|
if (!selected || forceSelection) {
|
|
|
|
tokenButton.classList.add('selected');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static removeSelectedToken() {
|
|
|
|
const selected = FilteredSearchContainer.container.querySelector('.js-visual-token .selected');
|
|
|
|
|
|
|
|
if (selected) {
|
|
|
|
const li = selected.closest('.js-visual-token');
|
|
|
|
li.parentElement.removeChild(li);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
static createVisualTokenElementHTML(canEdit = true) {
|
2017-08-17 22:00:37 +05:30
|
|
|
return `
|
2018-03-17 18:26:18 +05:30
|
|
|
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
|
2017-08-17 22:00:37 +05:30
|
|
|
<div class="name"></div>
|
|
|
|
<div class="value-container">
|
|
|
|
<div class="value"></div>
|
2018-03-17 18:26:18 +05:30
|
|
|
<div class="remove-token" role="button">
|
|
|
|
<i class="fa fa-close"></i>
|
|
|
|
</div>
|
2017-08-17 22:00:37 +05:30
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
static setTokenStyle(tokenContainer, backgroundColor, textColor) {
|
|
|
|
const token = tokenContainer;
|
|
|
|
|
|
|
|
// Labels with linear gradient should not override default background color
|
|
|
|
if (backgroundColor.indexOf('linear-gradient') === -1) {
|
|
|
|
token.style.backgroundColor = backgroundColor;
|
|
|
|
}
|
|
|
|
|
|
|
|
token.style.color = textColor;
|
|
|
|
|
|
|
|
if (textColor === '#FFFFFF') {
|
|
|
|
const removeToken = token.querySelector('.remove-token');
|
|
|
|
removeToken.classList.add('inverted');
|
|
|
|
}
|
|
|
|
|
|
|
|
return token;
|
|
|
|
}
|
|
|
|
|
|
|
|
static preprocessLabel(labelsEndpoint, labels) {
|
|
|
|
let processed = labels;
|
|
|
|
|
|
|
|
if (!labels.preprocessed) {
|
|
|
|
processed = gl.DropdownUtils.duplicateLabelPreprocessing(labels);
|
|
|
|
AjaxCache.override(labelsEndpoint, processed);
|
|
|
|
processed.preprocessed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return processed;
|
|
|
|
}
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
|
|
|
|
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
|
|
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
|
|
|
|
const labelsEndpoint = `${baseEndpoint}/labels.json`;
|
|
|
|
|
|
|
|
return AjaxCache.retrieve(labelsEndpoint)
|
2017-09-10 17:25:29 +05:30
|
|
|
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
|
|
|
|
.then((labels) => {
|
|
|
|
const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue);
|
|
|
|
|
|
|
|
if (!matchingLabel) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
FilteredSearchVisualTokens
|
|
|
|
.setTokenStyle(tokenValueContainer, matchingLabel.color, matchingLabel.text_color);
|
|
|
|
})
|
|
|
|
.catch(() => new Flash('An error occurred while fetching label colors.'));
|
|
|
|
}
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
|
|
|
|
if (tokenValue === 'none') {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
const username = tokenValue.replace(/^@/, '');
|
|
|
|
return UsersCache.retrieve(username)
|
|
|
|
.then((user) => {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* eslint-disable no-param-reassign */
|
|
|
|
tokenValueContainer.dataset.originalValue = tokenValue;
|
|
|
|
tokenValueElement.innerHTML = `
|
2018-03-17 18:26:18 +05:30
|
|
|
<img class="avatar s20" src="${user.avatar_url}" alt="">
|
|
|
|
${_.escape(user.name)}
|
2017-09-10 17:25:29 +05:30
|
|
|
`;
|
|
|
|
/* eslint-enable no-param-reassign */
|
|
|
|
})
|
|
|
|
// ignore error and leave username in the search bar
|
|
|
|
.catch(() => { });
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
2018-03-17 18:26:18 +05:30
|
|
|
static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
|
|
|
|
const container = tokenValueContainer;
|
|
|
|
const element = tokenValueElement;
|
|
|
|
|
|
|
|
return import(/* webpackChunkName: 'emoji' */ '../emoji')
|
|
|
|
.then((Emoji) => {
|
|
|
|
if (!Emoji.isEmojiNameValid(tokenValue)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
container.dataset.originalValue = tokenValue;
|
|
|
|
element.innerHTML = Emoji.glEmojiTag(tokenValue);
|
|
|
|
})
|
|
|
|
// ignore error and leave emoji name in the search bar
|
|
|
|
.catch(() => { });
|
|
|
|
}
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
|
|
|
|
const tokenValueContainer = parentElement.querySelector('.value-container');
|
2017-09-10 17:25:29 +05:30
|
|
|
const tokenValueElement = tokenValueContainer.querySelector('.value');
|
|
|
|
tokenValueElement.innerText = tokenValue;
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
const tokenType = tokenName.toLowerCase();
|
|
|
|
if (tokenType === 'label') {
|
2017-08-17 22:00:37 +05:30
|
|
|
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
|
2017-09-10 17:25:29 +05:30
|
|
|
} else if ((tokenType === 'author') || (tokenType === 'assignee')) {
|
|
|
|
FilteredSearchVisualTokens.updateUserTokenAppearance(
|
|
|
|
tokenValueContainer, tokenValueElement, tokenValue,
|
|
|
|
);
|
2018-03-17 18:26:18 +05:30
|
|
|
} else if (tokenType === 'my-reaction') {
|
|
|
|
FilteredSearchVisualTokens.updateEmojiTokenAppearance(
|
|
|
|
tokenValueContainer, tokenValueElement, tokenValue,
|
|
|
|
);
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
static addVisualTokenElement(name, value, isSearchTerm, canEdit) {
|
2017-08-17 22:00:37 +05:30
|
|
|
const li = document.createElement('li');
|
|
|
|
li.classList.add('js-visual-token');
|
|
|
|
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
|
|
|
|
|
|
|
|
if (value) {
|
2017-09-10 17:25:29 +05:30
|
|
|
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit);
|
2017-08-17 22:00:37 +05:30
|
|
|
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
|
|
|
|
} else {
|
|
|
|
li.innerHTML = '<div class="name"></div>';
|
|
|
|
}
|
|
|
|
li.querySelector('.name').innerText = name;
|
|
|
|
|
|
|
|
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
|
|
|
|
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
|
|
tokensContainer.insertBefore(li, input.parentElement);
|
|
|
|
}
|
|
|
|
|
|
|
|
static addValueToPreviousVisualTokenElement(value) {
|
|
|
|
const { lastVisualToken, isLastVisualTokenValid } =
|
|
|
|
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
|
|
|
|
if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
|
|
|
|
const name = FilteredSearchVisualTokens.getLastTokenPartial();
|
|
|
|
lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
|
|
|
|
lastVisualToken.querySelector('.name').innerText = name;
|
|
|
|
FilteredSearchVisualTokens.renderVisualTokenValue(lastVisualToken, name, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
|
2017-08-17 22:00:37 +05:30
|
|
|
const { lastVisualToken, isLastVisualTokenValid }
|
|
|
|
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
|
|
|
|
|
|
|
|
if (isLastVisualTokenValid) {
|
2017-09-10 17:25:29 +05:30
|
|
|
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
|
2017-08-17 22:00:37 +05:30
|
|
|
} else {
|
|
|
|
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
|
|
|
|
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
|
|
|
|
tokensContainer.removeChild(lastVisualToken);
|
|
|
|
|
|
|
|
const value = tokenValue || tokenName;
|
2017-09-10 17:25:29 +05:30
|
|
|
addVisualTokenElement(previousTokenName, value, false, canEdit);
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static addSearchVisualToken(searchTerm) {
|
|
|
|
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
|
|
|
|
if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
|
|
|
|
lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
|
|
|
|
} else {
|
|
|
|
FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static getLastTokenPartial() {
|
|
|
|
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
|
|
|
|
if (!lastVisualToken) return '';
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
const valueContainer = lastVisualToken.querySelector('.value-container');
|
|
|
|
const originalValue = valueContainer && valueContainer.dataset.originalValue;
|
|
|
|
if (originalValue) {
|
|
|
|
return originalValue;
|
|
|
|
}
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
const value = lastVisualToken.querySelector('.value');
|
|
|
|
const name = lastVisualToken.querySelector('.name');
|
|
|
|
|
|
|
|
const valueText = value ? value.innerText : '';
|
|
|
|
const nameText = name ? name.innerText : '';
|
|
|
|
|
|
|
|
return valueText || nameText;
|
|
|
|
}
|
|
|
|
|
|
|
|
static removeLastTokenPartial() {
|
|
|
|
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
|
|
|
|
if (lastVisualToken) {
|
|
|
|
const value = lastVisualToken.querySelector('.value');
|
|
|
|
|
|
|
|
if (value) {
|
|
|
|
const button = lastVisualToken.querySelector('.selectable');
|
|
|
|
const valueContainer = lastVisualToken.querySelector('.value-container');
|
|
|
|
button.removeChild(valueContainer);
|
|
|
|
lastVisualToken.innerHTML = button.innerHTML;
|
|
|
|
} else {
|
|
|
|
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static tokenizeInput() {
|
|
|
|
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
|
|
const { isLastVisualTokenValid } =
|
|
|
|
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
|
|
|
|
if (input.value) {
|
|
|
|
if (isLastVisualTokenValid) {
|
|
|
|
gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
|
|
|
|
} else {
|
|
|
|
FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
input.value = '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static editToken(token) {
|
|
|
|
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
|
|
|
|
|
|
FilteredSearchVisualTokens.tokenizeInput();
|
|
|
|
|
|
|
|
// Replace token with input field
|
|
|
|
const tokenContainer = token.parentElement;
|
|
|
|
const inputLi = input.parentElement;
|
|
|
|
tokenContainer.replaceChild(inputLi, token);
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
const nameElement = token.querySelector('.name');
|
|
|
|
let value;
|
2017-08-17 22:00:37 +05:30
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
if (token.classList.contains('filtered-search-token')) {
|
|
|
|
FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
|
|
|
|
|
|
|
|
const valueContainerElement = token.querySelector('.value-container');
|
|
|
|
value = valueContainerElement.dataset.originalValue;
|
|
|
|
|
|
|
|
if (!value) {
|
|
|
|
const valueElement = valueContainerElement.querySelector('.value');
|
|
|
|
value = valueElement.innerText;
|
|
|
|
}
|
2017-08-17 22:00:37 +05:30
|
|
|
}
|
|
|
|
|
2017-09-10 17:25:29 +05:30
|
|
|
// token is a search term
|
|
|
|
if (!value) {
|
|
|
|
value = nameElement.innerText;
|
|
|
|
}
|
|
|
|
|
|
|
|
input.value = value;
|
|
|
|
|
2017-08-17 22:00:37 +05:30
|
|
|
// Opens dropdown
|
|
|
|
const inputEvent = new Event('input');
|
|
|
|
input.dispatchEvent(inputEvent);
|
|
|
|
|
|
|
|
// Adds cursor to input
|
|
|
|
input.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
static moveInputToTheRight() {
|
|
|
|
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
|
|
|
|
|
|
|
|
if (!input) return;
|
|
|
|
|
|
|
|
const inputLi = input.parentElement;
|
|
|
|
const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
|
|
|
|
|
|
|
|
FilteredSearchVisualTokens.tokenizeInput();
|
|
|
|
|
|
|
|
if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
|
|
|
|
const { isLastVisualTokenValid } =
|
|
|
|
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
|
|
|
|
|
|
|
|
if (!isLastVisualTokenValid) {
|
|
|
|
const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
|
|
|
|
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
|
|
|
|
gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
|
|
|
|
}
|
|
|
|
|
|
|
|
tokenContainer.removeChild(inputLi);
|
|
|
|
tokenContainer.appendChild(inputLi);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
window.gl = window.gl || {};
|
|
|
|
gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens;
|