Disparando eventos em alterações de classe CSS no jQuery

240

Como posso disparar um evento se uma classe CSS for adicionada ou alterada usando o jQuery? A alteração de uma classe CSS dispara o change()evento jQuery ?

user237060
fonte
8
7 anos depois, com a ampla adoção de MutationObservers em navegadores modernos, a resposta aceita aqui deve ser realmente atualizada para ser do Sr. Br .
Joelmdev 15/06
Isso pode ajudá-lo. stackoverflow.com/questions/2157963/…
PSR
Em conclusão a resposta de Jason, eu encontrei este: stackoverflow.com/a/19401707/1579667
Benj

Respostas:

223

Sempre que você altera uma classe em seu script, você pode usar a triggerpara criar seu próprio evento.

$(this).addClass('someClass');
$(mySelector).trigger('cssClassChanged')
....
$(otherSelector).bind('cssClassChanged', data, function(){ do stuff });

mas, caso contrário, não, não há como preparar um evento quando uma classe muda. change()somente é acionado após o foco deixar uma entrada cuja entrada foi alterada.

$(function() {
  var button = $('.clickme')
      , box = $('.box')
  ;
  
  button.on('click', function() { 
    box.removeClass('box');
    $(document).trigger('buttonClick');
  });
            
  $(document).on('buttonClick', function() {
    box.text('Clicked!');
  });
});
.box { background-color: red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="box">Hi</div>
<button class="clickme">Click me</button>

Mais informações sobre jQuery Triggers

Jason
fonte
4
Qual é o problema de acionar o evento que chama uma função? Por que não chamar a função diretamente, em vez de acioná-la?
RamboNo5
43
O acionamento é um mecanismo que permitirá que certos elementos "se inscrevam" em um evento. Dessa forma, quando o evento é acionado, esses eventos podem acontecer de uma só vez e executar suas respectivas funcionalidades. por exemplo, se eu tiver um objeto que muda de vermelho para azul e três objetos esperando que ele mude, quando ele muda, posso acionar o changeColorevento e todos os objetos que assinam esse evento podem reagir de acordo.
23409 Jason
1
Parece um pouco estranho que o OP queira ouvir um evento 'cssClassChanged'; certamente a classe foi adicionada como resultado de algum outro evento 'application', como 'colourChanged', que faria mais sentido anunciar com trigger, pois não consigo imaginar por que algo estaria interessado especificamente em uma alteração de className, é mais resultado do classNameChange com certeza?
riscarrott
Se fosse especificamente uma mudança className que era de interesse, então eu imagino decorar jQuery.fn.addClass para gatilho 'cssClassChanged' seria mais sensato como @micred disse
riscarrott
5
@riscarrott Eu não presumo conhecer a aplicação do OP. Estou apenas apresentando a eles o conceito de pub / sub e eles podem aplicá-lo como quiserem. Disputar qual deve ser o nome do evento real com a quantidade de informações fornecidas é bobagem.
Jason
140

IMHO, a melhor solução é combinar duas respostas de @ RamboNo5 e @Jason

Quero dizer, substituindo a função addClass e adicionando um evento personalizado chamado cssClassChanged

// Create a closure
(function(){
    // Your base, I'm in it!
    var originalAddClassMethod = jQuery.fn.addClass;

    jQuery.fn.addClass = function(){
        // Execute the original method.
        var result = originalAddClassMethod.apply( this, arguments );

        // trigger a custom event
        jQuery(this).trigger('cssClassChanged');

        // return the original result
        return result;
    }
})();

// document ready function
$(function(){
    $("#YourExampleElementID").bind('cssClassChanged', function(){ 
        //do stuff here
    });
});
Arash Milani
fonte
Isso realmente soa como a melhor abordagem, mas, embora eu vincule o evento apenas a "#YourExampleElementID", parece que todas as alterações de classe (ou seja, em qualquer um dos elementos da minha página) são tratadas por esse manipulador ... alguma opinião?
Filipe Correia
Funciona bem para mim @FilipeCorreia. Você pode fornecer um jsfiddle para replicar seu caso?
Arash Milani
@ Goldentoa11 Você deve reservar um tempo para uma noite ou fim de semana e monitorar tudo o que acontece em um carregamento de página típico que usa muito a publicidade. Você ficará impressionado com o volume de scripts que tentam (às vezes sem êxito da maneira mais espetacular) fazer coisas assim. Corri por páginas que disparam dezenas de eventos DOMNodeInserted / DOMSubtreeModified por segundo, totalizando centenas de eventos no momento em que você chega $(), quando a ação real é iniciada.
L0j1k
Você presumivelmente também quer apoiar provocando o mesmo evento no removeClass
Darius
4
Arash, acho que entendo por que o @FilipeCorreia estava com alguns problemas com essa abordagem: se você vincular esse cssClassChangedevento a um elemento, deve estar ciente de que ele será acionado mesmo quando seu filho mudar de classe. Portanto, se, por exemplo, você não desejar acionar esse evento para eles, adicione algo como $("#YourExampleElementID *").on('cssClassChanged', function(e) { e.stopPropagation(); });após a ligação. Enfim, solução muito boa, obrigado!
tonix
59

Se você deseja detectar alterações de classe, a melhor maneira é usar os Observadores de Mutação, que oferecem controle total sobre qualquer alteração de atributo. No entanto, você mesmo precisa definir o ouvinte e anexá-lo ao elemento que está ouvindo. O bom é que você não precisa acionar nada manualmente depois que o ouvinte é anexado.

$(function() {
(function($) {
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

    $.fn.attrchange = function(callback) {
        if (MutationObserver) {
            var options = {
                subtree: false,
                attributes: true
            };

            var observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(e) {
                    callback.call(e.target, e.attributeName);
                });
            });

            return this.each(function() {
                observer.observe(this, options);
            });

        }
    }
})(jQuery);

//Now you need to append event listener
$('body *').attrchange(function(attrName) {

    if(attrName=='class'){
            alert('class changed');
    }else if(attrName=='id'){
            alert('id changed');
    }else{
        //OTHER ATTR CHANGED
    }

});
});

Neste exemplo, o ouvinte de evento é anexado a cada elemento, mas você não deseja isso na maioria dos casos (economize memória). Anexe esse ouvinte "attrchange" ao elemento que você deseja observar.

Mr Br
fonte
1
parece que está fazendo exatamente a mesma coisa que a resposta aceita, mas com mais código, sem suporte em alguns navegadores e com menos flexibilidade. o único benefício Isto parece ter é que ele dispara cada vez que algo muda na página, que com certeza vai destruir o seu desempenho ...
Jason
10
Sua declaração na resposta está incorreta @Json, é realmente assim que você deve fazer se não desejar ativar nenhum gatilho manualmente, mas ativá-lo automaticamente. Não é apenas benefício, é o benefício, porque isso foi questionado. Segundo, este foi apenas um exemplo e, como diz o exemplo, não é recomendável anexar ouvintes de eventos a todos os elementos, mas apenas aos elementos que você deseja ouvir, por isso não entendo por que isso destruirá o desempenho. E, por último, é futuro, a maioria dos navegadores mais recentes o suporta, caniuse.com/#feat=mutationobserver . Reconsidere a edição, é o caminho certo.
Sr. Br
1
A biblioteca insertionQ permite capturar nós DOM aparecendo se eles corresponderem a um seletor. Ele tem um suporte mais amplo ao navegador porque não usa DOMMutationObserver.
Dan Dascalescu 28/08/2015
Esta é uma otima soluçao. Especialmente para iniciar e parar animações de tela ou SVG quando uma classe é adicionada ou removida. No meu caso, para garantir que eles não estejam em execução quando rolados para fora da vista. Doce!
BBaysinger 24/08/19
1
Para 2019, essa deve ser a resposta aceita. Todos os principais navegadores parecem oferecer suporte a Mutation Observers agora, e esta solução não requer acionar eventos manualmente ou reescrever a funcionalidade básica do jQuery.
Sp00n
53

Eu sugiro que você substitua a função addClass. Você pode fazer assim:

// Create a closure
(function(){
    // Your base, I'm in it!
    var originalAddClassMethod = jQuery.fn.addClass;

    jQuery.fn.addClass = function(){
        // Execute the original method.
        var result = originalAddClassMethod.apply( this, arguments );

        // call your function
        // this gets called everytime you use the addClass method
        myfunction();

        // return the original result
        return result;
    }
})();

// document ready function
$(function(){
    // do stuff
});
RamboNo5
fonte
16
Combinar isso com o $ (mySelector) .trigger de Jason ('cssClassChanged') seria a melhor abordagem que eu acho.
kosoant
4
Existe um plugin que faz isso de forma genérica: github.com/aheckmann/jquery.hook/blob/master/jquery.hook.js
Jerph
37
Parece uma ótima maneira de confundir um futuro mantenedor.
roufamatic
1
Não se esqueça de retornar o resultado do método original.
Petah
1
Para referência, você está usando o Proxy Padrão en.wikipedia.org/wiki/Proxy_pattern
Darius
22

Apenas uma prova de conceito:

Veja a essência para ver algumas anotações e manter-se atualizado:

https://gist.github.com/yckart/c893d7db0f49b1ea4dfb

(function ($) {
  var methods = ['addClass', 'toggleClass', 'removeClass'];

  $.each(methods, function (index, method) {
    var originalMethod = $.fn[method];

    $.fn[method] = function () {
      var oldClass = this[0].className;
      var result = originalMethod.apply(this, arguments);
      var newClass = this[0].className;

      this.trigger(method, [oldClass, newClass]);

      return result;
    };
  });
}(window.jQuery || window.Zepto));

O uso é bastante simples, basta adicionar um novo listender no nó que você deseja observar e manipular as classes normalmente:

var $node = $('div')

// listen to class-manipulation
.on('addClass toggleClass removeClass', function (e, oldClass, newClass) {
  console.log('Changed from %s to %s due %s', oldClass, newClass, e.type);
})

// make some changes
.addClass('foo')
.removeClass('foo')
.toggleClass('foo');
yckart
fonte
Isso funcionou para mim, exceto que eu não conseguia ler a classe antiga ou nova com o método removeClass.
slashdottir
1
@slashdottir Você pode criar um violino e explicar o que exatamente não funciona .
yckart 18/09/14
Sim ... não funciona. TypeError: este [0] é indefinido em «retorne este [0] .className;»
Pedro Ferreira
ele funciona, mas às vezes (por quê?), this[0]é indefinido ... simplesmente substituir o final das linhas com var oldClasse var newClasscom a seguinte atribuição:= (this[0] ? this[0].className : "");
fvlinden
9

usando a mais recente mutação jquery

var $target = jQuery(".required-entry");
            var observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    if (mutation.attributeName === "class") {
                        var attributeValue = jQuery(mutation.target).prop(mutation.attributeName);
                        if (attributeValue.indexOf("search-class") >= 0){
                            // do what you want
                        }
                    }
                });
            });
            observer.observe($target[0],  {
                attributes: true
            });

// qualquer código que atualiza div com a classe required-entry que está em $ target como $ target.addClass ('search-class');

Hassan Ali Shahzad
fonte
Ótima solução, estou usando. No entanto, estou usando o react e um dos problemas é que adiciono um observador toda vez que o elemento da web é virtualmente gerado. Existe uma maneira de verificar se um observador já está presente?
Mikaël Mayer
Eu acho que isso foi depreciado e não deve ser usado de acordo com [link] developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/…
WebDude0482
@ WebDude0482 MutationObserver.observe não foi preterido, pois é um "Método de Instância" do "MutationObserver". Veja developer.mozilla.org/pt-BR/docs/Web/API/MutationObserver
nu everest
8

change()não é acionado quando uma classe CSS é adicionada ou removida ou a definição é alterada. É acionado em circunstâncias como quando um valor da caixa de seleção é selecionado ou desmarcado.

Não tenho certeza se você quer dizer se a definição da classe CSS foi alterada (o que pode ser feito através de programação, mas é tedioso e geralmente não recomendado) ou se uma classe é adicionada ou removida a um elemento. Não há como capturar isso de maneira confiável nos dois casos.

Obviamente, você poderia criar seu próprio evento para isso, mas isso só pode ser descrito como um aviso . Ele não captura o código que não é seu.

Como alternativa, você pode substituir / substituir os addClass()métodos (etc) no jQuery, mas isso não será capturado quando for feito via Javascript de baunilha (embora eu ache que você também possa substituir esses métodos).

cleto
fonte
8

Há mais uma maneira sem acionar um evento personalizado

Um plug-in jQuery para monitorar alterações de CSS de elemento HTML por Rick Strahl

Citando acima

O plug-in de inspeção funciona conectando DOMAttrModified no FireFox, onPropertyChanged no Internet Explorer ou usando um timer com setInterval para lidar com a detecção de alterações em outros navegadores. Infelizmente, o WebKit não suporta DOMAttrModified de forma consistente no momento, portanto, o Safari e o Chrome atualmente precisam usar o mecanismo setInterval, mais lento.

Amitd
fonte
6

Boa pergunta. Estou usando o menu suspenso Bootstrap e precisava executar um evento quando um menu suspenso do Bootstrap estava oculto. Quando o menu suspenso é aberto, a div que contém o nome de uma classe de "grupo de botões" adiciona uma classe de "aberto"; e o botão em si possui um atributo "ária-expandido" definido como true. Quando o menu suspenso é fechado, essa classe de "aberto" é removida da div que contém e a ária-expandida é alterada de verdadeiro para falso.

Isso me levou a essa pergunta, de como detectar a mudança de classe.

Com o Bootstrap, há "Eventos suspensos" que podem ser detectados. Procure por "Eventos suspensos" neste link. http://www.w3schools.com/bootstrap/bootstrap_ref_js_dropdown.asp

Aqui está um exemplo rápido e sujo de usar esse evento em um menu suspenso de inicialização.

$(document).on('hidden.bs.dropdown', function(event) {
    console.log('closed');
});

Agora percebo que isso é mais específico do que a pergunta geral que está sendo feita. Mas imagino que outros desenvolvedores que tentam detectar um evento aberto ou fechado com um menu suspenso do Bootstrap achem isso útil. Como eu, eles podem inicialmente seguir o caminho de simplesmente tentar detectar uma alteração de classe de elemento (o que aparentemente não é tão simples). Obrigado.

Ken Palmer
fonte
5

Se você precisar disparar um evento específico, poderá substituir o método addClass () para disparar um evento personalizado chamado 'classadded'.

Aqui como:

(function() {
    var ev = new $.Event('classadded'),
        orig = $.fn.addClass;
    $.fn.addClass = function() {
        $(this).trigger(ev, arguments);
        return orig.apply(this, arguments);
    }
})();

$('#myElement').on('classadded', function(ev, newClasses) {
    console.log(newClasses + ' added!');
    console.log(this);
    // Do stuff
    // ...
});
Micred
fonte
5

Você pode vincular o evento DOMSubtreeModified. Eu adiciono um exemplo aqui:

HTML

<div id="mutable" style="width:50px;height:50px;">sjdfhksfh<div>
<div>
  <button id="changeClass">Change Class</button>
</div>

Javascript

$(document).ready(function() {
  $('#changeClass').click(function() {
    $('#mutable').addClass("red");
  });

  $('#mutable').bind('DOMSubtreeModified', function(e) {
      alert('class changed');
  });
});

http://jsfiddle.net/hnCxK/13/

Diva Sativa
fonte
0

se você conhece um evento que mudou a classe em primeiro lugar, pode usar um pequeno atraso no mesmo evento e verificar a classe. exemplo

//this is not the code you control
$('input').on('blur', function(){
    $(this).addClass('error');
    $(this).before("<div class='someClass'>Warning Error</div>");
});

//this is your code
$('input').on('blur', function(){
    var el= $(this);
    setTimeout(function(){
        if ($(el).hasClass('error')){ 
            $(el).removeClass('error');
            $(el).prev('.someClass').hide();
        }
    },1000);
});

http://jsfiddle.net/GuDCp/3/

Nikos Tsagkas
fonte
0
var timeout_check_change_class;

function check_change_class( selector )
{
    $(selector).each(function(index, el) {
        var data_old_class = $(el).attr('data-old-class');
        if (typeof data_old_class !== typeof undefined && data_old_class !== false) 
        {

            if( data_old_class != $(el).attr('class') )
            {
                $(el).trigger('change_class');
            }
        }

        $(el).attr('data-old-class', $(el).attr('class') );

    });

    clearTimeout( timeout_check_change_class );
    timeout_check_change_class = setTimeout(check_change_class, 10, selector);
}
check_change_class( '.breakpoint' );


$('.breakpoint').on('change_class', function(event) {
    console.log('haschange');
});
Jérome Obbiet
fonte