UNPKG

@govbr-ds/core

Version:

Padrão Digital de Governo

836 lines (803 loc) 34.6 kB
/** * Classe do componente BRSelect */ class BRSelect { /** * Instancia o componente * @property {string} name - Nome do componente em minúsculo * @property {object} component - Objeto referenciando a raiz do componente DOM * @property {string} notFound - (Opcional) String contendo código html para informar que a busca não encontrou nenhuma opção. * Se não informado, será usado um código html padrão. */ constructor(name, component, notFound) { this.name = name this.component = component this.notFound = notFound this.multiple = component.hasAttribute('multiple') this._setOptionsList() this._setBehavior() } /** * Retorna os valores dos elementos selecionados * @returns {Object[]} elementos selecionados * */ get selected() { return this._optionSelected('value') } /** * Retorna os elementos selecionados pelo inputValue * @param {Object[]} elementos selecionados * @returns */ get selectedValue() { return this._optionSelected('inputValue') } /** * Retorna os elementos options selecionados * @param {Object[]} elementos selecionados * @returns */ _optionSelected(strOption) { let selected = [] for (const [index, option] of this.optionsList.entries()) { if (!this.multiple) { if (option.selected) { selected = option[strOption] break } } else { if (index > 0 && option.selected) { selected.push(option[strOption]) } } } return selected } /** * Remove o elemento que indica item não encontrado * @private */ _removeNotFoundElement() { const list = this.component.querySelector('.br-list') if (list.querySelector('.br-item.not-found')) { list.removeChild(list.querySelector('.br-item.not-found')) } } /** * Coloca o texto Item não encontrado no select * @private */ _addNotFoundElement() { const image = 'data:image/png;base64,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' const notFoundElement = ` <div class="br-item not-found"> <div class="container pl-0 pr-0"> <div class="row"> <div class="col-auto"> <img src="${image}"> </div> <div class="col"> <p>Não encontramos o que você está procurando!</p> </div> </div> </div> </div> ` const list = this.component.querySelector('.br-list') list.insertAdjacentHTML('beforeend', this.notFound ? this.notFound : notFoundElement) } /** * Cria listagem de elementos do select * @private */ _setOptionsList() { this.optionsList = [] for (const item of this.component.querySelectorAll('.br-list .br-item')) { for (const input of item.querySelectorAll('.br-radio input, .br-checkbox input')) { const option = { element: item, focus: false, inputValue: input.value, selected: false, value: input.nextElementSibling.innerText, visible: true, } this.optionsList.push(option) } } } /** * Reseta estado da lista * @private */ resetOptionsList() { this._unsetSelectionBehavior() this._setOptionsList() this._setSelectionBehavior() this._setAria() } /** * Define o comportamento do componente * @private */ _setBehavior() { this._setSearchIcon() this._setDropdownBehavior() this._setKeyboardBehavior() this._setSelectionBehavior() this._setFilterBehavior() this._prepareComponent() } /** * Define o comportamento de dropdown * @private */ _setDropdownBehavior() { for (const input of this.component.querySelectorAll('.br-input input[type="text"]')) { input.addEventListener('focus', () => { this._resetFocus() }) input.addEventListener('click', () => { this._openSelect() this._resetFocus() }) } for (const trigger of this.component.querySelectorAll('.br-input .br-button[data-trigger]')) { trigger.addEventListener('click', () => { for (const list of this.component.querySelectorAll('.br-list')) { if (list.hasAttribute('expanded')) { this._closeSelect() } else { this._openSelect() } } }) } window.document.addEventListener('click', (event) => { if (!this.component.contains(event.target)) { this._closeSelect() } }) } /** * Define o comportamento de teclado * @private */ _setKeyboardBehavior() { for (const input of this.component.querySelectorAll('.br-input input[type="text"]')) { input.addEventListener('keydown', this._handleKeydownOnInput.bind(this)) } for (const list of this.component.querySelectorAll('.br-list')) { // eslint-disable-next-line complexity list.addEventListener('keydown', this._handleKeydownOnList.bind(this)) } } _prepareComponent() { const inputElement = this.component.querySelector('.br-input') const buttonElement = this.component.querySelector('.br-input .br-button') const listElement = this.component.querySelector('.br-list') listElement.setAttribute('id', `list-${Math.floor(Math.random() * 10000)}`) listElement.setAttribute('role', 'listbox') listElement.setAttribute('tabindex', '-1') listElement.setAttribute('aria-label', 'Lista de Opções') inputElement.setAttribute('role', 'combobox') inputElement.setAttribute('aria-expanded', false) inputElement.setAttribute('aria-controls', `${listElement.id}`) inputElement.setAttribute('aria-autocomplete', 'list') buttonElement.setAttribute('aria-controls', `${listElement.id}`) buttonElement.setAttribute('aria-expanded', false) buttonElement.setAttribute('aria-label', 'Exibir lista') this.component.querySelectorAll('.br-list .br-item').forEach((item) => { item.setAttribute('role', 'option') }) if (this.multiple) { inputElement.setAttribute('aria-multiselectable', true) } } /** * Retira o comportamento de teclado * @private */ _unsetKeyboardBehavior() { for (const input of this.component.querySelectorAll('.br-input input[type="text"]')) { input.removeEventListener('keydown', this._handleKeydownOnInput, false) } for (const list of this.component.querySelectorAll('.br-list')) { // eslint-disable-next-line complexity list.addEventListener('keydown', this._handleKeydownOnList.bind(this)) } } /** * Verifica a navegação * @param {object} event evento que foi ativado * @private */ _handleKeydownOnInput(event) { switch (event.key) { case 'Tab': this._closeSelect() this._resetFocus() break case 'ArrowDown': event.preventDefault() this._openSelect() this._getNextItem().focus() break case 'ArrowUp': event.preventDefault() this._openSelect() this._getPreviousItem().focus() break case 'Escape': this._closeSelect() break case 'Enter': this.component.querySelector('.br-list').hasAttribute('expanded') ? this._closeSelect() : this._openSelect() break default: } } /** * Define comportamentos de teclado na lista * @private */ _handleKeydownOnList(event) { event.preventDefault() switch (event.key) { case 'Tab': this._closeSelect() this._resetFocus() break case 'Escape': for (const input of this.component.querySelectorAll('.br-input input[type="text"]')) { input.focus() } this._closeSelect() break case 'Enter': this._setKeyClickOnOption(event.currentTarget) break case ' ': this._setKeyClickOnOption(event.currentTarget) break case 'ArrowUp': this._getPreviousItem().focus() break case 'ArrowDown': this._getNextItem().focus() break default: break } } /** * Define comportamentos de teclado no option * @private */ _setKeyClickOnOption(list) { for (const [index, item] of list.querySelectorAll('.br-item').entries()) { if (this.optionsList[index].focus) { for (const check of item.querySelectorAll( '.br-radio input[type="radio"], .br-checkbox input[type="checkbox"]' )) { check.click() this._sendEvent() } } } } /** * Envia o evento onchange * @private */ _sendEvent() { const clickEvent = new CustomEvent('onChange', { bubbles: true, detail: this.component, }) this.component.dispatchEvent(clickEvent) } /** * preseleciona o elemento apartir da classe css .selected * @private */ _setDefaultSelected() { this.component.querySelectorAll('.br-list .br-item').forEach((item) => { const itemIsSelected = item.classList.contains('selected') || item.querySelector('input').hasAttribute('checked') if (itemIsSelected) { this._setSelected(this._positionSelected(item), item) } }) } /** * Retorna posição do elemento no select * @param {element} component elemento que vai ser pesquisado * @returns {integer} valor da posição */ _positionSelected(component) { for (const [index, componente] of this.component.querySelectorAll('.br-list .br-item').entries()) { if (componente === component) { return index } } return 0 } /** * Desfine comportamento do clique no checkbox * @param {int} index * @param {object} item - Objeto do item clicado * @param {object} event - Objeto do evento do clique * @private */ _handleClickOnCheck(index, item, event) { if (!this.multiple) { for (const [index2, item2] of this.component.querySelectorAll('.br-list .br-item').entries()) { this._removeSelected(index2, item2) } this._setSelected(index, item) this._closeSelect() } else if (event.currentTarget.hasAttribute('checked')) { this._removeSelected(index, item) } else { this._setSelected(index, item) } if (item.hasAttribute('data-all')) { for (const check of item.querySelectorAll('.br-checkbox input[type="checkbox"]')) { if (!check.hasAttribute('checked')) { this._setAttribute() item.querySelectorAll('label')[0].innerText = 'Selecionar Todos' } else { for (const item2 of this.component.querySelectorAll('.br-list .br-item')) { for (const check2 of item2.querySelectorAll('.br-checkbox input[type="checkbox"]')) { if (!check2.hasAttribute('checked')) { check2.click() } } } item.querySelectorAll('label')[0].innerText = 'Deselecionar Todos' } } } this._sendEvent() } /** * Define comportamentos na seleção * @private */ _setSelectionBehavior() { this.selectionHandler = [] this._setDefaultSelected() for (const [index, item] of this.component.querySelectorAll('.br-list .br-item').entries()) { for (const check of item.querySelectorAll('.br-radio input[type="radio"], .br-checkbox input[type="checkbox"]')) { this.selectionHandler.push({ element: check, handler: this._handleClickOnCheck.bind(this, index, item), }) check.addEventListener('click', this.selectionHandler[index].handler) } } } /** * retira comportamento de clique na seleção * @private */ _unsetSelectionBehavior() { this.selectionHandler.forEach((item) => { item.element.removeEventListener('click', item.handler, false) }) } /** * Define comportamentos no filtro do input * @private */ _setFilterBehavior() { const list = this.component.querySelector('.br-list') for (const input of this.component.querySelectorAll('.br-input input[type="text"]')) { input.addEventListener('input', (event) => { if (!list.hasAttribute('expanded')) { const { value } = event.currentTarget this._openSelect() event.currentTarget.value = value } let allHidden = true this._filter(event.currentTarget.value) for (const option of this.optionsList) { if (option.visible) { allHidden = false } } if (allHidden) { // event.currentTarget.value = event.currentTarget.value.slice(0, -1) this._filter(event.currentTarget.value) } }) } } /** * Define filtro no list * @private */ _filter(value) { let hasVisible = false for (const [index, item] of this.component.querySelectorAll('.br-list .br-item').entries()) { this._removeNotFoundElement() if (!this.optionsList[index]) { continue } if (this.optionsList[index].value.toUpperCase().indexOf(value.toUpperCase()) === -1) { item.classList.add('d-none') this.optionsList[index].visible = false } else { item.classList.remove('d-none') this.optionsList[index].visible = true hasVisible = true } } if (hasVisible === false) { this._addNotFoundElement() } else { this._removeNotFoundElement() } } /** * Define atributo checked com click * @private */ _setAttribute() { for (const item2 of this.component.querySelectorAll('.br-list .br-item')) { for (const check2 of item2.querySelectorAll('.br-checkbox input[type="checkbox"]')) { if (check2.hasAttribute('checked')) { check2.click() } } } } /** * Seleciona o elemento e retira checked dos outros elementos * @param {integer} index Posição do elemento na lista * @param {*} item elemento em que vai ser selecionado * @private */ _setSelected(index, item) { item.classList.add('selected') item.setAttribute('aria-selected', true) for (const check of item.querySelectorAll('.br-radio, .br-checkbox')) { for (const input of check.querySelectorAll('input[type="radio"], input[type="checkbox"]')) { input.setAttribute('checked', '') } } this.optionsList[index].selected = true this._setInput() } /** * Retira o estado selecionado do elemento * @param {integer} index Posição do elemento na lista * @param {*} item elemento em que vai ser desselecionado * @private */ _removeSelected(index, item) { item.classList.remove('selected') item.removeAttribute('aria-selected') for (const check of item.querySelectorAll('.br-radio, .br-checkbox')) { for (const input of check.querySelectorAll('input[type="radio"], input[type="checkbox"')) { input.removeAttribute('checked') } this.optionsList[index].selected = false this._setInput() } } /** * Determina o input * @private */ _setInput() { for (const input of this.component.querySelectorAll('.br-input input[type="text"]')) { if (!this.multiple) { input.value = this.selected } else if (this.selected.length === 0) { input.value = '' } else { this.mountSelectedValues(input) } } } /** * Monta a string apresentada no campo input do multiselect * @param {object} input Referencia o elemento input do select */ mountSelectedValues(input) { this.amountE = this.amountE > 0 ? this.amountE : 0 const amount = this.amountE const value = this.selected.toString().replaceAll(',', ', ') const tempSpan = document.createElement('span') tempSpan.innerHTML = value document.querySelector('body').insertAdjacentElement('beforeend', tempSpan) const maxCharacterCount = this.getMaxCharacterCountBeforeOverflow(input) this.amountE = amount input.title = this.selected.toString().replaceAll(',', ', ') const ct = this.encontrarPosicaoPorTamanho(this.selected, maxCharacterCount - 6) input.value = ct tempSpan.remove() } /** * Obtém o tamanho em pixels de um valor de texto. * * @param {string} value - O valor de texto para o qual se deseja obter o tamanho em pixels. * @returns {number} O tamanho em pixels do valor de texto. */ getPixelSize(value) { const span = document.createElement('span') span.style.visibility = 'hidden' span.style.position = 'absolute' span.style.left = '-9999px' span.style.top = '-9999px' span.style.whiteSpace = 'nowrap' span.textContent = value document.body.appendChild(span) const size = span.getBoundingClientRect().width document.body.removeChild(span) return size } /** * Retorna uma string contendo elementos de um array até o limite de caracteres especificado. * Se houver elementos truncados, a quantidade de elementos truncados é indicada no final da string. * * @param {Array<string>} array - O array de elementos. * @param {number} maxCharacterCount - O limite máximo de caracteres permitidos na string resultante. * @returns {string} - A string contendo os elementos até o limite de caracteres, com indicação de elementos truncados, se houver. */ encontrarPosicaoPorTamanho(array, maxCharacterCount) { let account = 0 let value = '' let acountVisible = 0 let posUltimoArray = 0 for (const element of array) { const posicaoArray = array.indexOf(element) account += 1 if (element.length >= maxCharacterCount && array.length === 1) { return `${element.substring(0, maxCharacterCount)}... ` } if (value.length >= maxCharacterCount) { value += `${element}, ` value = `${value.substring(0, maxCharacterCount)}...` acountVisible = acountVisible === 0 ? 1 : acountVisible let contOverflow = posicaoArray - acountVisible contOverflow = acountVisible === 0 ? array.length - 1 : array.length - acountVisible posUltimoArray = contOverflow } else { const tmpValue = posicaoArray > 0 ? `, ${element}` : `${element}` value += `${tmpValue}` acountVisible += 1 } } value = posUltimoArray <= 0 ? `${value}` : `${value} + (${posUltimoArray})` return value } /** * Retorna a quantidade máxima de caracteres de um elemento de input antes do overflow. * * @param {HTMLElement} input - O elemento de input. * @returns {number} A quantidade máxima de caracteres antes do overflow. */ getMaxCharacterCountBeforeOverflow(input) { const style = window.getComputedStyle(input) const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) const border = parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth) const availableWidth = input.clientWidth - padding - border const averageCharacterWidth = this.getAverageCharacterWidth(input) return Math.floor(availableWidth / averageCharacterWidth) } /** * Retorna a largura média de um caractere dentro de um elemento de input. * * @param {HTMLElement} input - O elemento de input. * @returns {number} A largura média de um caractere. */ getAverageCharacterWidth(input) { const tempSpan = document.createElement('span') tempSpan.style.visibility = 'hidden' tempSpan.style.position = 'absolute' tempSpan.style.whiteSpace = 'nowrap' tempSpan.textContent = 'X' document.body.appendChild(tempSpan) const averageWidth = tempSpan.offsetWidth document.body.removeChild(tempSpan) return averageWidth } /** * Filtra um array removendo elementos que ultrapassem o tamanho do input. Adiciona "..." aos elementos truncados. * * @param {Array} array - O array a ser filtrado. * @param {HTMLElement} input - O elemento de input usado como referência para o tamanho máximo. * @returns {Array} O array filtrado. */ filterArrayByInputSize(array, input) { const maxLength = getMaxCharacterCountBeforeOverflow(input) const filteredArray = array.filter((element) => { const elementText = element.toString() if (getPixelSize(elementText) <= input.offsetWidth) { return true } else { const truncatedText = `${elementText.substring(0, maxLength - 3)}...` input.value = truncatedText return false } }) return filteredArray } /** * Retorna elemento posterior ao focado * @returns {object} * @private */ // eslint-disable-next-line complexity _getNextItem() { const list = this.component.querySelectorAll('.br-list .br-item') let iFocused let iVisible for (iFocused = 0; iFocused < this.optionsList.length; iFocused++) { if (this.optionsList[iFocused].focus) { for (iVisible = iFocused + 1; iVisible < this.optionsList.length; iVisible++) { if (this.optionsList[iVisible].visible) { break } } break } } if (iFocused === this.optionsList.length) { for (let index = 0; index < this.optionsList.length; index++) { if (this.optionsList[index].visible) { this.optionsList[index].focus = true return list[index] } } } else if (iVisible < this.optionsList.length) { this.optionsList[iFocused].focus = false this.optionsList[iVisible].focus = true return list[iVisible] } else { return list[iFocused] } return '' } /** * Retorna elemento anterior ao focado * @returns {object} * @private */ _getPreviousItem() { const list = this.component.querySelectorAll('.br-list .br-item') let iFocused let iVisible for (iFocused = 0; iFocused < this.optionsList.length; iFocused++) { if (this.optionsList[iFocused].focus) { for (iVisible = iFocused - 1; iVisible > 0; iVisible--) { if (this.optionsList[iVisible].visible) { break } } break } } if (iFocused === this.optionsList.length) { for (let index = this.optionsList.length - 1; index >= 0; index--) { if (this.optionsList[index].visible) { this.optionsList[index].focus = true return list[index] } } } else if (iFocused === 0) { return list[iFocused] } else { this.optionsList[iFocused].focus = false this.optionsList[iVisible].focus = true return list[iVisible] } return '' } /** * Reseta valor do input * @private */ _resetInput() { for (const input of this.component.querySelectorAll('.br-input input[type="text"]')) { input.value = '' } } /** * Reseta o focus dos elementos * @private */ _resetFocus() { for (const option of this.optionsList) { option.focus = false } } /** * Reseta o focus dos elementos visiveis * @private */ _resetVisible() { this._removeNotFoundElement() const list = this.component.querySelectorAll('.br-list .br-item') for (const [index, option] of this.optionsList.entries()) { option.visible = true list[index].classList.remove('d-none') } } /** * Abre o select aberto * @private */ _openSelect() { const inputElement = this.component.querySelector('.br-input') const buttonElement = this.component.querySelector('.br-input .br-button') for (const list of this.component.querySelectorAll('.br-list')) { list.setAttribute('expanded', '') } for (const icon of this.component.querySelectorAll('.br-input .br-button i')) { icon.classList.remove('fa-angle-down') icon.classList.add('fa-angle-up') } inputElement.setAttribute('aria-expanded', true) buttonElement.setAttribute('aria-expanded', true) buttonElement.setAttribute('aria-label', 'Ocultar lista') this._resetInput() } /** * Fecha o select aberto * @private */ _closeSelect() { const inputElement = this.component.querySelector('.br-input') const buttonElement = this.component.querySelector('.br-input .br-button') for (const list of this.component.querySelectorAll('.br-list')) { list.removeAttribute('expanded') } for (const icon of this.component.querySelectorAll('.br-input .br-button i')) { icon.classList.remove('fa-angle-up') icon.classList.add('fa-angle-down') } inputElement.setAttribute('aria-expanded', false) buttonElement.setAttribute('aria-expanded', false) buttonElement.setAttribute('aria-label', 'Exibir lista') this._setInput() this._resetFocus() this._resetVisible() } /** * Adiciona ícone de busca * @private */ _setSearchIcon() { const brInput = this.component.querySelector('.br-input') const dropButton = this.component.querySelector('[data-trigger]') // Ícone de busca const searchIcon = document.createElement('i') searchIcon.classList.add('fas', 'fa-search') searchIcon.setAttribute('aria-hidden', 'true') // Container para o ícone const inputIcon = document.createElement('div') inputIcon.classList.add('input-icon') inputIcon.appendChild(searchIcon) // Estrutura para input com ícone const inputGroup = document.createElement('div') inputGroup.classList.add('input-group') inputGroup.appendChild(inputIcon) inputGroup.appendChild(brInput.querySelector('input')) brInput.appendChild(inputGroup) brInput.appendChild(dropButton) } _setAria() { const inputElement = this.component.querySelector('.br-input') const listElement = this.component.querySelector('.br-list') inputElement.setAttribute('role', 'combobox') inputElement.setAttribute('aria-expanded', false) inputElement.setAttribute('aria-controls', `${listElement.id}`) listElement.setAttribute('role', 'listbox') } } export default BRSelect