input file 文件选择的取消事件

示例

原生的input标签无法监听取消事件, 我们通过对容器的blur事件和click事件, 以及input的change事件, 三者结合进行判断:

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>选择文件的取消事件</title>
    <style>
        * {
            font-size: large;
        }
    </style>
</head>

<body>
    <div>
        <button id="btn">选择文件</button>
    </div>
    <script>
        addFileSelect(
            btn,
            /* 选择文件事件 */
            (input) => {
                alert('您选择了文件: ' + input.files[0].name);
            },
            /* 取消选择事件 */
            () => {
                alert('您取消了文件选择');
            }
        );

        /**
         * 为容器添加文件选择事件, 容器通常是一个按钮
         */
        function addFileSelect(container, onselect, oncancel) {
            // <input type="file">
            let input = document.createElement('input'); input.type = 'file';
            // states
            let waiting = false; // 是否尚在等待选择文件
            let clicked = false; // 按钮是否被点击
            container.addEventListener('click', () => {
                clicked = true; // 按钮被点击
                input.click(); // 弹窗
                waiting = true; // 等待用户选择文件, 此时按钮会失去焦点
            });
            container.addEventListener('blur', () => {
                if (clicked && waiting) {
                    clicked = false; // 用户点击容器后, 容器会失去一次焦点, 此时处于waiting状态
                    // waiting没有被input的change事件置为false, 却触发了blur的失焦事件
                } else if (waiting) { // 容器再次失去焦点, 仍旧处于waiting状态, 断言用户取消了选择
                    console.log('blur事件测试到用户取消了选择');
                    oncancel?.();
                }
            });
            input.addEventListener('change', () => {
                waiting = false; // 检测到用户选择了文件
                if (input.value === '') { // 此时, 用户肯定点击了取消按钮, 否则value不会变为空串, 而且之前肯定选择过文件, 否则不会触发change事件
                    console.log('change事件感知到用户取消了选择');
                    oncancel?.();
                } else {
                    onselect?.(input);
                }
            });
        }
    </script>
</body>

</html>

算法改进: blur的对立事件: focus

在回忆上午完成的代码时, 我发现我们需要手动点击容器之外的UI使其产生blur事件才能检测到取消事件, 但是弹窗时由于容器失去焦点导致已经产生过一次该事件了呀?
原来是系统自动将焦点放到容器上了! 当我们的文件选择框无论因为以下哪种原因关闭的时候, 容器都会自动获得blur事件:

  • 用户选择了一个文件
  • 用户取消了选择文件
    所以我们是可以立即判断的! 加入容器的焦点事件, 发现焦点事件先于点击事件之前触发.
    但是change事件可能排在最后!
focus => click => blur => 弹窗 => (A or B) => 弹窗关闭 => focus =>? change

最关键的点是什么? 我也很混乱

但是没有关系, 我还是找到了关键点, 由于change事件可能排在最后, 因此要在弹窗关闭 => focus中判断时不能依赖input的事件.
但是此时input的值肯定已经发生了变化, 如果用户取消了选择, 那么input的值肯定是空串???

由于事件过于复杂, 实际上我们只关心点击之后的事情, 所以在容器的click事情中添加后续的事件监听器. 事件全部只监听一次:

容器点击事件 => 容器失去焦点 => 容器获得焦点 =>? input改变事件

只有input的change事件是不稳定的.

解决方案

         /**
         * 为容器添加文件选择事件, 容器通常是一个按钮
         */
        function addFileSelect(container, onselect, oncancel) {
            container.addEventListener('click', () => {
                let input = document.createElement('input'); input.type='file';
                input.click();
                let selected = false;
                let onchange = null; // 取消选择时不会触发change事件, 需要手动移除监听器

                container.addEventListener('focus', () => {
                    console.log(input.value); // 大概先于onchange事件100ms执行, 所以一定是空串
                    // 当取消选择时则不会触发onchange事件
                    let close_time = new Date(); // 记录弹窗关闭的时间
                    // 轮询
                    (function loop() {
                        let crt_time = new Date(); // 查询时间
                        if (selected) {
                            onselect?.(input);
                        } else if (crt_time - close_time > 1000) { // 该时间不确保一定可以触发change事件
                            input.removeEventListener('change', onchange);
                            oncancel?.();
                        } else {
                            setTimeout(loop, 20);
                        };
                    })();
                }, { once: true });

                input.addEventListener('change', onchange = () => {
                    console.log('change');
                    selected = true;
                }, { once: true });
            });
        }

我们甚至可以丢弃change事件, 同时基于轮询次数判断取消, 而不是基于时间:

        /**
         * 为容器添加文件选择事件, 容器通常是一个按钮
         */
        function addFileSelect(container, onselect, oncancel) {
            container.addEventListener('click', () => {
                let input = document.createElement('input'); input.type='file';
                input.click();

                container.addEventListener('focus', () => {
                    console.log(input.value); // 大概先于onchange事件100ms执行, 所以一定是空串
                    let loop_count = 0; // 轮询次数
                    // 轮询
                    (function loop() {
                        if (input.value !== '') { // 不需要change事件
                            onselect?.(input);
                        } else if (++loop_count >= 10) { // 基于轮询次数的判断
                            oncancel?.();
                        } else { // 暂时无法判断, 继续轮询
                            setTimeout(loop, 20);
                        };
                    })();
                }, { once: true });
            });
        }
posted @ 2020-09-23 14:25  develon  阅读(7591)  评论(1编辑  收藏  举报