Как предотвратить регистрацию одного и того же события на элементах-предках, когда Event.stopPropagation не работает?

У меня есть расширяемое меню с вложенными подменю на бесконечную глубину.

<ul>
    <li>One</li>
    <li class="contains-submenu">Two
        <ul>
            <li>Two A</li>
            <li class="contains-submenu">Two B
                <ul>
                    <li>Two B I</li>
                    <li>Two B II</li>
                    <li>Two B III</li>
                </ul>
            </li>
            <li>Two C</li>
        </ul>
    </li>
    <li>Three</li>
    <li class="contains-submenu">Four
        <ul>
            <li>Four A</li>
            <li class="contains-submenu">Four B
                <ul>
                    <li>Four B I</li>
                    <li>Four B II</li>
                    <li>Four B III</li>
                </ul>
            </li>
            <li>Four C</li>
        </ul>
    </li>
</ul>

Я хочу показать / скрыть любое подменю на любом уровне, не влияя на статус показа / скрытия любого другого подменю и особенно на состояние показа / скрытия любого предка подменю.

Первоначально я обнаружил, что если бы я использовал click прослушиватель событий для переключения класса .show-submenu чтобы показать или скрыть подменю, когда нижнее подменю (например, Two B) было закрыто, его родитель, подменю Two, также закрылось бы.

Стало очевидным, что это происходило, потому что почти в тот же момент, когда я нажимал на Two B, событие нажатия также регистрировалось на Two (... конечно, это было - Two B вложено в Two - любой щелчок на Two B будет также зарегистрируйтесь как клик на Два).

Я пытался исправить это с помощью .stopPropagation()... но потом я понял, что на самом деле это был (по крайней мере, я так не думаю) случай, когда одно событие регистрировалось на двух разных элементах, когда событие вспыхнуло через два из двух B, а два разных события регистрируясь один на двоих почти сразу за другим на двоих Б.

Мое решение было следующее:

var activateToggle = true;

function toggleSubmenu() {
    if (activateToggle === true) {
        this.classList.toggle('show-submenu');
        activateToggle = false;
    }

    setTimeout(function(){activateToggle = true}, 100);
}

Это разумный подход для получения желаемого эффекта?

Или есть более традиционный подход с использованием event.target или похожие?

Полный рабочий пример:

var querySelector = 'ul > li > ul';
var selectedElements = [... document.querySelectorAll(querySelector)];
var activateToggle = true;
var initialPadding = 6;
var subsequentPadding = 18;
var level = 0;


while (selectedElements.length > 0) {

    for (var j = 0; j < selectedElements.length; j++) {
        selectedElements[j].parentNode.classList.add('contains-submenu');
        selectedElements[j].style.marginLeft = (0 - (initialPadding + (level * subsequentPadding))) + 'px';
        [... selectedElements[j].children].forEach(function(childElement){
            childElement.style.paddingLeft = (initialPadding + ((level + 1) * subsequentPadding)) + 'px';
        });
    }

    level++;
    querySelector += ' > li > ul';
    selectedElements = [... document.querySelectorAll(querySelector)];
}

var containsSubmenus = document.getElementsByClassName('contains-submenu');


function toggleSubmenu() {
    if (activateToggle === true) {
        this.classList.toggle('show-submenu');
        activateToggle = false;
    }

    setTimeout(function(){activateToggle = true}, 100);
}


for (let i = 0; i < containsSubmenus.length; i++) {
    containsSubmenus[i].addEventListener('click', toggleSubmenu, false);
}
ul {
margin-left: 0;
padding-left: 0;
font-family: arial, helvetica, sans-serif;
color: rgb(255, 255, 255);
background-color: rgba(0, 75, 165, 1);
list-style-type: none;
}

li {
padding-left: 6px;
font-size: 16px;
line-height: 32px;
}

ul ul {
display: none;
}

.show-submenu > ul {
display: block;
}

li.show-submenu {
height: auto;
}

.contains-submenu {
font-weight: 700;
cursor: pointer;
}

.contains-submenu ul {
font-weight: 300;
cursor: default;
}
.contains-submenu,
.contains-submenu ul {
background-color: rgba(255, 255, 255, 0.1);
}
<ul>
    <li>One</li>
    <li class="contains-submenu">Two
        <ul>
            <li>Two A</li>
            <li class="contains-submenu">Two B
                <ul>
                    <li>Two B I</li>
                    <li>Two B II</li>
                    <li>Two B III</li>
                </ul>
            </li>
            <li>Two C</li>
        </ul>
    </li>
    <li>Three</li>
    <li class="contains-submenu">Four
        <ul>
            <li>Four A</li>
            <li class="contains-submenu">Four B
                <ul>
                    <li>Four B I</li>
                    <li>Four B II</li>
                    <li>Four B III</li>
                </ul>
            </li>
            <li>Four C</li>
        </ul>
    </li>
</ul>

1 ответ

Решение

Вот решение, объединяющее event.target а также event.stopPropagation,

Вы можете проверить, является ли event.target на самом деле содержит подменю и, если так, переключите его и остановите распространение события.

Это ожидаемое поведение?

var querySelector = 'ul > li > ul';
var selectedElements = [... document.querySelectorAll(querySelector)];
var initialPadding = 6;
var subsequentPadding = 18;
var level = 0;


while (selectedElements.length > 0) {

    for (var j = 0; j < selectedElements.length; j++) {
        selectedElements[j].parentNode.classList.add('contains-submenu');
        selectedElements[j].style.marginLeft = (0 - (initialPadding + (level * subsequentPadding))) + 'px';
        [... selectedElements[j].children].forEach(function(childElement){
            childElement.style.paddingLeft = (initialPadding + ((level + 1) * subsequentPadding)) + 'px';
        });
    }

    level++;
    querySelector += ' > li > ul';
    selectedElements = [... document.querySelectorAll(querySelector)];
}

var containsSubmenus = document.getElementsByClassName('contains-submenu');


function toggleSubmenu(event) {     
    if (event.target.classList.contains('contains-sublist')) {
     event.target.classList.toggle('show-sublist');
     event.stopPropagation();
    } else {
     console.log("LEAF"); 
    }
}


for (let i = 0; i < containsSubmenus.length; i++) {
    containsSubmenus[i].addEventListener('click', toggleSubmenu, false);
}
ul {
margin-left: 0;
padding-left: 0;
font-family: arial, helvetica, sans-serif;
color: rgb(255, 255, 255);
background-color: rgba(0, 75, 165, 1);
list-style-type: none;
}

li {
padding-left: 6px;
font-size: 16px;
line-height: 32px;
}

ul ul {
display: none;
}

.show-submenu > ul {
display: block;
}

li.show-submenu {
height: auto;
}

.contains-submenu {
font-weight: 700;
cursor: pointer;
}

.contains-submenu ul {
font-weight: 300;
cursor: default;
}
.contains-submenu,
.contains-submenu ul {
background-color: rgba(255, 255, 255, 0.1);
}
<ul>
    <li>One</li>
    <li class="contains-submenu">Two
        <ul>
            <li>Two A</li>
            <li class="contains-submenu">Two B
                <ul>
                    <li>Two B I</li>
                    <li>Two B II</li>
                    <li>Two B III</li>
                </ul>
            </li>
            <li>Two C</li>
        </ul>
    </li>
    <li>Three</li>
    <li class="contains-submenu">Four
        <ul>
            <li>Four A</li>
            <li class="contains-submenu">Four B
                <ul>
                    <li>Four B I</li>
                    <li>Four B II</li>
                    <li>Four B III</li>
                </ul>
            </li>
            <li>Four C</li>
        </ul>
    </li>
</ul>

РЕДАКТИРОВАТЬ: ответы на ваши вопросы

Способ, которым вы присоединяете слушателей событий, делает две вещи:

  1. Поскольку вы прослушиваете клики по элементам, содержащим подменю, каждый элемент в подсписке будет запускать по 1 событию на каждого предка. Например, нажатие на "Two B II" (у которого есть два предка "содержит подменю") вызовет два события. И вы не хотите переключать флаг дважды.
  2. Вы кипите. Итак, когда вы нажмете на "лист" (элемент, который не содержит подменю), вы сначала обработаете событие для элемента "лист". И вы не хотите ничего делать при обработке события click для такого элемента. Вместо этого вы хотите, чтобы это пузырилось.

По этим причинам я проверяю classList, прежде чем переключать подсписок и останавливать распространение. Сюда:

  1. Мы хотим остановить распространение события только в том случае, если его целью является элемент "содержит-подменю", чтобы предотвратить появление пузырей у другого предка "содержит-подменю".
  2. Мы не хотим предотвращать пузырение, когда целью является "листовой" элемент.
  3. Если цель события не является элементом "содержит-подменю", мы не хотим переключать какой-либо подсписок.
Другие вопросы по тегам