javascript - 当 Event.stopPropagation 不起作用时,如何防止在祖先元素上注册相同的事件?

标签 javascript addeventlistener 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>

我想显示/隐藏任何级别的任何子菜单,而不影响任何其他子菜单的显示/隐藏状态,尤其是<的显示/隐藏状态em>子菜单的任何祖先

我最初发现的是,如果我使用 click 事件监听器来切换类 .show-submenu 来显示或隐藏子菜单,当子菜单向下时(例如 Two B)被关闭,那么它的父菜单,子菜单 Two,也会关闭

很明显,这种情况正在发生,因为几乎在我点击 Two B 的同时,点击事件也在 Two 上注册(...当然是这样 - Two B 嵌套在 Two 内部Two - 任何对 Two B 的点击也将注册为一次点击在)。

我尝试使用 .stopPropagation() 修复此问题...但后来我意识到实际上这不是(至少我不认为它是) ) 当事件从 Two B 中通过 Two 向上冒泡时,单个事件在两个不同的元素上注册,但注册了两个不同的事件,一个在 Two 上,几乎紧接着另一个在 Two 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>

最佳答案

这是一个结合了 event.targetevent.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”(它有两个“contains-submenu”祖先)将触发两个事件。而且您不想两次切换任何标志。
  2. 你正在冒泡。因此,一旦您单击“叶子”(不包含子菜单的元素),您将首先处理叶子元素的事件。并且您不想在处理此类元素上的单击事件时执行任何操作。相反,你希望它冒泡。

出于这些原因,我在切换子列表并停止传播之前检查 classList。这样:

  1. 我们只想在事件的目标是“contains-submenu”元素时停止事件的传播,以防止冒泡到另一个“contains-submenu”祖先。
  2. 当目标是“叶子”元素时,我们不想阻止冒泡。
  3. 如果事件目标不是“contains-submenu”元素,我们不想切换任何子列表。

关于javascript - 当 Event.stopPropagation 不起作用时,如何防止在祖先元素上注册相同的事件?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46913283/

相关文章:

javascript - 如何在 React JS 中选择性地启用/禁用表格中的按钮?

javascript - addEventListener 创建了一个无限循环

javascript - 使用 ES6 箭头函数设置事件监听器

javascript - 单击元素但在元素内部的内容上启动事件

javascript - jQuery如何加载图像的一部分

javascript - 如何使用 jQuery 禁用文本选择?

angular - ios 模拟器中出现 ionic /Angular FileReader 错误(addEventListener 不是函数)

jQuery:当底层元素使用 "event.stopPropagation()"设置单击事件时了解 "find()"

javascript - 停止传播 : bubble down?

javascript - AngularJS – 从一次性绑定(bind)中减去双向绑定(bind)