Actions
Padrão #33
openCheckbox diretiva hover e click
Added by jean sodré 6 months ago. Updated 6 months ago.
Start date:
07/09/2025
Due date:
% Done:
0%
Estimated time:
Description
Identifiquei um bug, quando tento selecionar uma opção de select dentro de um container e o checkbox está marcado, ao clicar no select, desmarca o checkbox
Updated by jean sodré 6 months ago
- Copied from Padrão #31: Ajustar diretiva Popover Para Funcionar com campos Select added
Updated by jean sodré 6 months ago · Edited
- Status changed from em execução to teste
Diretiva finalizada:
.directive('hoverEclique', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.addClass('hoverEclique');
// Função para verificar se o input está selecionado
function verificarSelecao() {
const input = element.find('input')[0];
if (!input) return;
let estaSelecionado = false;
if (input.type === 'checkbox') {
estaSelecionado = input.checked;
} else if (input.type === 'radio') {
// Para radio, verifica se o ng-model tem o mesmo valor do input
const ngModel = angular.element(input).attr('ng-model');
if (ngModel) {
const modelValue = scope.$eval(ngModel);
estaSelecionado = (modelValue === input.value);
} else {
estaSelecionado = input.checked;
}
}
if (estaSelecionado) {
element.addClass('selecionado');
element[0].style.backgroundColor = '#F0F0F0';
} else {
element.removeClass('selecionado');
element[0].style.backgroundColor = '';
}
}
// Função para limpar seleção dos siblings (para radio buttons)
function limparSiblings() {
const input = element.find('input')[0];
if (input && input.type === 'radio') {
const name = input.name;
if (name) {
// Busca todos os inputs radio com o mesmo name
const radios = document.querySelectorAll(`input[name="${name}"]`);
radios.forEach(radio => {
const container = radio.closest('.hoverEclique');
if (container && container !== element[0]) {
container.classList.remove('selecionado');
container.style.backgroundColor = '';
}
});
}
}
}
function isInteractiveElement(clickedElement) {
if (!clickedElement) return false;
// Lista de elementos e classes que devem ser ignorados
const interactiveSelectors = [
// Custom-select
'.custom-select-wrapper',
'.select-header',
'.select-options-body',
'.option',
'.arrow-icon',
'custom-select',
// Popover
'.popover-edit-icon',
'.popover-container',
'.popover-content',
'.popover-body',
// Elementos HTML interativos
'button',
'select',
'textarea',
'input[type="text"]',
'input[type="number"]',
'input[type="email"]',
'input[type="password"]',
'input[type="search"]',
'input[type="url"]',
'input[type="tel"]',
'input[type="date"]',
'input[type="datetime-local"]',
'input[type="month"]',
'input[type="time"]',
'input[type="week"]',
'input[type="color"]',
'input[type="range"]',
'input[type="file"]',
// Elementos com atributos específicos
'[contenteditable]',
'[data-interactive]',
'[data-no-hover-click]'
];
// Verifica se o elemento clicado ou algum de seus pais corresponde aos seletores
let currentElement = clickedElement;
while (currentElement && currentElement !== element[0]) {
// Verifica tag name
const tagName = currentElement.tagName ? currentElement.tagName.toLowerCase() : '';
// Verifica se é um elemento interativo por tag
if (['button', 'select', 'textarea', 'input'].includes(tagName)) {
// Para input, verifica se não é o input principal (checkbox/radio)
if (tagName === 'input') {
const inputPrincipal = element.find('input')[0];
if (currentElement !== inputPrincipal) {
return true;
}
} else {
return true;
}
}
// Verifica se é um custom-select por tag
if (tagName === 'custom-select') {
return true;
}
// Verifica classes CSS
if (currentElement.classList) {
for (let selector of interactiveSelectors) {
if (selector.startsWith('.')) {
const className = selector.substring(1);
if (currentElement.classList.contains(className)) {
return true;
}
}
}
}
// Verifica atributos específicos
if (currentElement.hasAttribute && (
currentElement.hasAttribute('contenteditable') ||
currentElement.hasAttribute('data-interactive') ||
currentElement.hasAttribute('data-no-hover-click')
)) {
return true;
}
// Verifica se tem data-custom-select (para elementos do custom-select)
if (currentElement.hasAttribute && (
currentElement.hasAttribute('data-custom-select') ||
currentElement.hasAttribute('data-custom-select-header') ||
currentElement.hasAttribute('data-custom-select-dropdown') ||
currentElement.hasAttribute('data-custom-select-option')
)) {
return true;
}
currentElement = currentElement.parentElement;
}
return false;
}
// Verifica estado inicial
$timeout(function() {
verificarSelecao();
}, 300);
// Verificação adicional para mudanças programáticas
let intervalId = setInterval(function() {
const input = element.find('input')[0];
if (input) {
const estaChecked = input.checked;
const temClasse = element.hasClass('selecionado');
// Se há divergência entre o estado do input e a classe visual
if (estaChecked !== temClasse) {
scope.$apply(function() {
if (input.type === 'radio') {
limparSiblings();
}
verificarSelecao();
});
}
}
}, 200);
// Limpa o interval quando o elemento é destruído
scope.$on('$destroy', function() {
if (intervalId) {
clearInterval(intervalId);
}
});
// Monitora mudanças no input
const input = element.find('input')[0];
if (input) {
// Escuta mudanças no input
angular.element(input).on('change', function() {
$timeout(function() {
if (input.type === 'radio') {
limparSiblings();
}
verificarSelecao();
}, 10);
});
// Escuta cliques diretos no input
angular.element(input).on('click', function() {
$timeout(function() {
if (input.type === 'radio') {
limparSiblings();
}
verificarSelecao();
}, 10);
});
// Monitora mudanças no ng-model - versão mais robusta
const ngModel = angular.element(input).attr('ng-model');
if (ngModel) {
scope.$watch(ngModel, function(newVal, oldVal) {
// Força a verificação sempre que o modelo muda
$timeout(function() {
// Para radio buttons, limpa todos os siblings primeiro
if (input.type === 'radio') {
const name = input.name;
if (name) {
const radios = document.querySelectorAll(`input[name="${name}"]`);
radios.forEach(radio => {
const container = radio.closest('.hoverEclique');
if (container) {
container.classList.remove('selecionado');
container.style.backgroundColor = '';
}
});
}
}
// Depois verifica e aplica no correto
verificarSelecao();
}, 50);
});
// Watch adicional para mudanças profundas no scope
scope.$watch(function() {
return scope.$eval(ngModel);
}, function(newVal, oldVal) {
if (newVal !== oldVal) {
$timeout(function() {
if (input.type === 'radio') {
const name = input.name;
if (name) {
const radios = document.querySelectorAll(`input[name="${name}"]`);
radios.forEach(radio => {
const container = radio.closest('.hoverEclique');
if (container) {
container.classList.remove('selecionado');
container.style.backgroundColor = '';
}
});
}
}
verificarSelecao();
}, 50);
}
}, true);
}
}
element.on('click', function(event) {
const input = element.find('input')[0];
if (!input) return;
const clickedElement = event.target;
if (isInteractiveElement(clickedElement)) {
return;
}
// Verifica se o clique foi diretamente no input ou em um label associado
const isInputClick = clickedElement === input;
const isLabelClick = clickedElement.tagName === 'LABEL' &&
(clickedElement.getAttribute('for') === input.id ||
clickedElement.contains(input) ||
input.closest('label') === clickedElement);
// Se o clique foi no input ou label, deixa o comportamento nativo acontecer
// e apenas sincroniza o visual depois
if (isInputClick || isLabelClick) {
$timeout(function() {
if (input.type === 'radio') {
limparSiblings();
}
verificarSelecao();
}, 10);
return;
}
if (clickedElement.closest('custom-select') ||
clickedElement.closest('.custom-select-wrapper') ||
clickedElement.closest('.popover-edit-icon') ||
clickedElement.closest('button') ||
clickedElement.closest('select') ||
clickedElement.closest('textarea') ||
clickedElement.closest('input:not([type="checkbox"]):not([type="radio"])')) {
return;
}
event.preventDefault();
event.stopPropagation();
scope.$apply(function() {
if (input.type === 'checkbox') {
// Para checkbox, inverte o estado
input.checked = !input.checked;
// Sincroniza o ng-model se existir
const ngModel = angular.element(input).attr('ng-model');
if (ngModel) {
scope.$eval(ngModel + ' = ' + input.checked);
}
} else if (input.type === 'radio') {
// Para radio, sempre marca como true
input.checked = true;
const ngModel = angular.element(input).attr('ng-model');
if (ngModel && input.value) {
scope.$eval(ngModel + ' = "' + input.value + '"');
}
limparSiblings();
}
const changeEvent = new Event('change', { bubbles: true });
input.dispatchEvent(changeEvent);
// Atualiza o visual
verificarSelecao();
});
});
// Cleanup
scope.$on('$destroy', function() {
element.off('click');
const input = element.find('input')[0];
if (input) {
angular.element(input).off('change');
angular.element(input).off('click');
}
});
}
};
})
Actions