import { last } from 'lodash'; import FilteredSearchContainer from './container'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; import FilteredSearchTokenizer from './filtered_search_tokenizer'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class DropdownUtils { static getEscapedText(text) { let escapedText = text; const hasSpace = text.indexOf(' ') !== -1; const hasDoubleQuote = text.indexOf('"') !== -1; // Encapsulate value with quotes if it has spaces // Known side effect: values's with both single and double quotes // won't escape properly if (hasSpace) { if (hasDoubleQuote) { escapedText = `'${text}'`; } else { // Encapsulate singleQuotes or if it hasSpace escapedText = `"${text}"`; } } return escapedText; } static filterWithSymbol(filterSymbol, input, item) { const updatedItem = item; const searchInput = DropdownUtils.getSearchInput(input); const title = updatedItem.title.toLowerCase(); let value = searchInput.toLowerCase(); let symbol = ''; // Remove the symbol for filter if (value[0] === filterSymbol) { [symbol] = value; value = value.slice(1); } // Removes the first character if it is a quotation so that we can search // with multiple words if ((value[0] === '"' || value[0] === "'") && title.indexOf(' ') !== -1) { value = value.slice(1); } // Eg. filterSymbol = ~ for labels const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1; const match = title.indexOf(`${symbol}${value}`) !== -1; updatedItem.droplab_hidden = !match && !matchWithoutSymbol; return updatedItem; } static filterHint(config, item) { const { input, allowedKeys } = config; const updatedItem = item; const searchInput = DropdownUtils.getSearchQuery(input); const { lastToken, tokens } = FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); const lastKey = lastToken.key || lastToken || ''; const allowMultiple = item.type === 'array'; const itemInExistingTokens = tokens.some((t) => t.key === item.hint); const isSearchItem = updatedItem.hint === 'search'; if (isSearchItem) { updatedItem.droplab_hidden = true; } if (!allowMultiple && itemInExistingTokens) { updatedItem.droplab_hidden = true; } else if (!isSearchItem && (!lastKey || last(searchInput.split('')) === ' ')) { updatedItem.droplab_hidden = false; } else if (lastKey) { const split = lastKey.split(':'); const tokenName = last(split[0].split(' ')); const match = isSearchItem ? allowedKeys.some((key) => key.startsWith(tokenName.toLowerCase())) : updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; updatedItem.droplab_hidden = tokenName ? match : false; } return updatedItem; } static setDataValueIfSelected(filter, operator, selected) { const dataValue = selected.getAttribute('data-value'); if (dataValue) { FilteredSearchDropdownManager.addWordToInput({ tokenName: filter, tokenOperator: operator, tokenValue: dataValue, clicked: true, options: { capitalizeTokenValue: selected.hasAttribute('data-capitalize'), }, }); } // Return boolean based on whether it was set return dataValue !== null; } static getVisualTokenValues(visualToken) { const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim(); let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); if (tokenName === 'label' && tokenValue) { // remove leading symbol and wrapping quotes tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); } const operatorEl = visualToken && visualToken.querySelector('.operator'); const tokenOperator = operatorEl && operatorEl.textContent.trim(); return { tokenName, tokenOperator, tokenValue }; } // Determines the full search query (visual tokens + input) static getSearchQuery(untilInput = false) { const { container } = FilteredSearchContainer; const tokens = [].slice.call(container.querySelectorAll('.tokens-container li')); const values = []; if (untilInput) { const inputIndex = tokens.findIndex((t) => t.classList.contains('input-token')); // Add one to include input-token to the tokens array tokens.splice(inputIndex + 1); } tokens.forEach((token) => { if (token.classList.contains('js-visual-token')) { const name = token.querySelector('.name'); const operatorContainer = token.querySelector('.operator'); const value = token.querySelector('.value'); const valueContainer = token.querySelector('.value-container'); const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; let valueText = ''; let operator = ''; if (operatorContainer) { operator = operatorContainer.textContent.trim(); } if (valueContainer && valueContainer.dataset.originalValue) { valueText = valueContainer.dataset.originalValue; } else if (value && value.innerText) { valueText = value.innerText; } if (token.className.indexOf('filtered-search-token') !== -1) { values.push(`${name.innerText.toLowerCase()}:${operator}${symbol}${valueText}`); } else { values.push(name.innerText); } } else if (token.classList.contains('input-token')) { const { isLastVisualTokenValid, } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const inputValue = input && input.value; if (isLastVisualTokenValid) { values.push(inputValue); } else { const previous = values.pop(); values.push(`${previous}${inputValue}`); } } }); return values.map((value) => value.trim()).join(' '); } static getSearchInput(filteredSearchInput) { const inputValue = filteredSearchInput.value; const { right } = DropdownUtils.getInputSelectionPosition(filteredSearchInput); return inputValue.slice(0, right); } static getInputSelectionPosition(input) { const { selectionStart } = input; let inputValue = input.value; // Replace all spaces inside quote marks with underscores // (will continue to match entire string until an end quote is found if any) // This helps with matching the beginning & end of a token:key inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, (str) => str.replace(/\s/g, '_'), ); // Get the right position for the word selected // Regex matches first space let right = inputValue.slice(selectionStart).search(/\s/); if (right >= 0) { right += selectionStart; } else if (right < 0) { right = inputValue.length; } // Get the left position for the word selected // Regex matches last non-whitespace character let left = inputValue.slice(0, right).search(/\S+$/); if (selectionStart === 0) { left = 0; } else if (selectionStart === inputValue.length && left < 0) { left = inputValue.length; } else if (left < 0) { left = selectionStart; } return { left, right, }; } }