JS. Всплытие и погружение события

Что такое всплытие и погружение события? Так же известное как эффект бабблинга.

Этот эффект можно наблюдать если одно и то же событие происходит на элементе и на его родительском элементе.

Например есть блок с событием 'click', внутри него также есть блок с событием 'click', внутри него также есть блок с событием 'click' и так далее. Все события в нашем случае меняют цвет блока на красный, а через 0,5 секунды возвращают обратно.

    block.addEventListener( 'click', callback, false );

    function callback(event){
        let target = event.currentTarget;
        let ms = ( event.timeout = event.timeout + 500 || 0 )
        setTimeout(function(){
            target.classList.add('light');
            setTimeout(function(){
                target.classList.remove('light');
            }, 500);
        }, ms);
    }
                    

Как можете заметить при нажатии на внутренний элемент, каждый из его родителей так же меняет свой цвет. Это происходит из за эффекта баблинга, или всплытие пузырька. Дело в том что если задать событие через addEventListener с третим параметром "false", то изначально отработает событие на самом нижнем элементе на который был произведен клик. После этого сработает событие на элементе выше на один уровень, и так до самого document и window.

Так работает всплытие. Если же мы хотим перехватить погружение события, а оно так же имеется. По скольку в действительности событие происходит в 2 этапа. Сначала событие срабатывает на родительском элементе, после чего опускается на дочерние элементы, после достижения самого нижнего элемента событие начинает подниматься вновь к самому верхнему элементу. Но по умолчанию погружение игнорируется и мы видим то что видим на примере выше.

Так как же нам перехватить событие погружения? Для этого третьим параметром в addEventListener необходимо передать "true". На самом деле третьим параметром передается целый обьект с настройками у которого свойство "capture" установлено в "true", но исторически сложилось что просто "true" = { capture: true }

    block.addEventListener( 'click', callback, true );

    function callback(event){
        let target = event.currentTarget;
        let ms = ( event.timeout = event.timeout + 500 || 0 )
        setTimeout(function(){
            target.classList.add('light');
            setTimeout(function(){
                target.classList.remove('light');
            }, 500);
        }, ms);
    }
                

Теперь как видите все происходит наоборот, от родительского элемента до элемента по которому был произведен клик.

И на последок сделаем так что бы отрабатывали оба события, и погружение, и всплытие.

    block.addEventListener( 'click', callback, true );
    block.addEventListener( 'click', callback, false );

    function callback(event){
        let target = event.currentTarget;
        let ms = ( event.timeout = event.timeout + 500 || 0 )
        setTimeout(function(){
            target.classList.add('light');
            setTimeout(function(){
                target.classList.remove('light');
            }, 500);
        }, ms);
    }
                

Для этого нужно повесить 2 события на один и тот же эллемент, просто в одно задать "true" третьим параметром, а в другом "false". Вот и вся премудрость!

Как прервать всплытие?

На практике применение погружения встречается довольно редко. А вот то что действительно встречается повсеместно, так это отмена всплытия. Часто случается так что событие клика присутствует у родительского элемента и у дочернего, но нам не нужно что бы при клике на дочерний элемент срабатывало событие родителя. Сдесь нам на помощь приходит метод события stopPropagation().

Например у нас есть блок в котором есть кнопка при клике на которую открывается/закрывается всплывающий блок. Стандартная ситуация например для скрытого меню.

    (function(){
        const button = document.querySelector('.button');
        const subblock = document.querySelector('.subblock');
    
        button.addEventListener('click', e => subblock.classList.toggle('hide'));
    })();
                

И все вроде бы хорошо. Но что если мы захотим сделать так что бы при клике на пространство вокруг всплывающего блока он так же закрывался? Такой эффект так же часто реализован на реальных пректах.

Ничего сложного скажете вы. Давайте просто на родительский элемент добавим событие клика, которое будет закрывать всплывающий блок. Логично?

    (function(){
        const button = document.querySelector('.button');
        const subblock = document.querySelector('.subblock');
        const container = document.querySelector('.container');
    
        button.addEventListener('click', e => subblock.classList.toggle('hide'));
        container.addEventListener('click', e => subblock.classList.remove('hide'));
    })();
                

Но вот ведь незадача, всё перестало работать. Нет, никакой ошибки в коде нет, код отрабатывает ровно так как ему указали. Сначала срабатывает событие клика по кнопке, оно открывает всплывающий блок, а сразу после этого происходит всплытие события и отрабатывает событие родительского элемента, которое опять скрывает всплывающий блок. Всё это происходит настолько быстро, что человеческий глаз не видит никакой разницы, поэтому кажется что ничего не происходит.

Сдесь нам на помощь приходит метод который останавливает всплытие события stopPropagation(). Каждый элемент клик по которому должен быть единственным(не передавать событие родительским элементам) должен иметь событие с методом stopPropagation(). Так мы получим ровно то, что хотели изначально.

    (function(){
        const button = document.querySelector('.button');
        const subblock = document.querySelector('.subblock');
        const container = document.querySelector('.container');
    
        subblock.addEventListener('click', e => {
            e.stopPropagation();
        });
        button.addEventListener('click', e => {
            subblock.classList.toggle('hide');
            e.stopPropagation();
        });
        container.addEventListener('click', e => subblock.classList.remove('hide'));
    })();
                

Теперь всё работает как нужно, кнопка открывает и закрывает, клики в области всплывающего блока так же не вызывают его закрытия. А вот клик в оставшейся незанятой области закрывают всплывающий блок. То что мы и хотели!