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 });
            });
        }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM