mouseenter/leave vs mouseover/out

有这样一个需求:给页面中table添加下载为excel表格的功能。我想到的做法就是鼠标hover时,在表格左上角显示一个按钮,点击按钮下载。下载为excel表格文件的代码网上已经有了,所以只要关注hover显示按钮的实现。

做法一:DOM载入完成后给其中的所有table元素分别append一个按钮,初始处于隐藏状态,通过css控制hover时显示。这样做需要修改css代码,虽然并不复杂,但是多余。其次,也是最关键的,处理不到js生成的table元素,除非在以前的代码里再加入append按钮的代码,简直自虐。所以:

做法二:事件委托。

先用jQuery实现一下:

$(document)
    .on('mouseenter', '.table', function() {
        var $this = $(this).css('position', 'relative'); // table设置position为relative,按钮为absolute
        var $btn = $('<button class="btn btn-success btn-sm table-excel-btn" style="position:absolute;left:0;top:0;z-index:10">Excel</button>').on('click', function() {
            var name = $this[0].title || 'sheet';
            var filename = ($this[0].title || 'table') + '.xls';
            tableToExcel($this[0], name, filename);
        });
        $this.append($btn)
    }).on('mouseleave', '.table', function() {
        $(this).children('.table-excel-btn').remove();
    })

很简单是吧。为什么这里用mouseenter/leave而不是mouseover/out,其实写这段代码的时候还不清楚两者的区别,只知道用后者的话,鼠标移到按钮上按钮会不停闪烁,至于为什么这样后面再分析。

然后需求又加了一条不使用jQuery。

好吧,改成原生的:

document.addEventListener('mouseenter', function(e) {
    var el = e.target;
    while (el && el.classList && el.classList.contains('table')) {
        if (el === this) {
            el = null;
            break;
        }
        el = el.parentElement;
    }
    if (el) {
        // 添加按钮
    }
});
// mouseleave事件绑定

改完运行发现行不通,handler只会在鼠标移入和移出页面时触发,e.target都是document,移入或移出页面内元素时不会触发。再改用mouseover/out试试,结果和用jQuery时一样鬼畜。

这才专门去搜两者的区别 —— mouseenter/leave不冒泡,mouseover/out冒泡。所以上面的代码当然只会作用于document元素。

前面按钮闪烁的问题,因为按钮是table的子元素,当鼠标移到按钮上时,以table左上角元素(一般是单元格th或td,此时被按钮遮挡)为目标元素的mouseout事件产生并冒泡到document,触发mouseout的handler,按钮被删除,于是鼠标指针又进入了单元格,以单元格为目标元素的mouseover事件产生并冒泡到document,触发mouseover的handler,按钮再被添加上去,如此循环。

问题来了,事件委托是利用事件的冒泡机制实现的,那jQuery是怎么做到给不冒泡的mouseenter/leave事件进行委托的呢?答案是用mouseover/out模拟实现mouseenter/leave,参见源码

需要比较目标元素和事件的relatedTarget这一属性 —— 当它不是目标元素的子元素或等于null(鼠标移入或移出浏览器窗口时)时,此时的事件才相当于mouseenter/leave。

完成后的代码:

document.addEventListener('mouseover', function(e) {
    var el = e.target;
    while (el && el.classList && el.classList.contains('table')) {
        if (el === this) {
            el = null;
            break;
        }
        el = el.parentElement;
    }
    if ( el && 
        (!e.relatedTarget || !el.contains(e.relatedTarget)) &&
        el.querySelector('tbody') && el.querySelectorAll('tbody > tr').length // 额外判断,table必须有tbody且tbody至少有一行
    ) {
        // 添加按钮
    }
});
document.addEventListener('mouseout', function(e) {
    var el = e.target;
    while (el && el.classList && el.classList.contains('table')) {
        if (el === this) {
            el = null;
            break;
        }
        el = el.parentElement;
    }
    if ( el && ( !e.relatedTarget || !el.contains(e.relatedTarget) ) ) {
        // 删除按钮
    }
});

另外,table转excel文件的代码也需要一些修改,否则中文会显示为乱码。文件后缀是xls,但其实是xml文件,需要在head标签中加入

<meta http-equiv="content-type" content="application/vnd.ms-excel; charset=UTF-8"></meta>

指定utf-8编码,参考这里。另外还要把添进去的按钮元素去掉,以及把每个td元素中的html内容替换为纯文本:td.innerHTML = td.innerText

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据