是否有一种 modern/new 方式来处理 JavaScript 的文件选择对话框?

Is there a modern/new way of handling file selection dialogs with JavaScript?

这是我的具体用例:我想点击文件对话框的 "Cancel" 按钮。原因是我经常使用 Promises,我希望在选择文件时解决 promise,但在取消对话框时拒绝(或用 null 解决)。

然而,我在这里和其他地方发现的这个问题的每个 "solution" 几乎都只是一个(大部分不可靠的)解决方法,比如捕捉 body 的焦点事件。
我自己的解决方法是将当前的 Promises 拒绝函数保存在一个(模块局部)全局变量中,并在第二次调用该函数时调用它,因此至少可以在第二个对话框中选择文件时实现 Promise。

问题是:是否有一种现代的、可用的方法来处理这种情况,而不是解决方法?如果有,那是什么?

这是我当前的代码,使用 TypeScript 和 JQuery:

export function selectJsonFile() {
    return new Promise<File>((resolve, reject) => {
        let $input = $(`<input type="file" accept=".json">`);
        $input.hide().change(() => {
            let file = (<HTMLInputElement>$input[0]).files[0];
            if (file)
                resolve(file);
            else
                reject();
            $input.remove();
        }).appendTo($("body")).click();
    });
}

这是我刚刚写的纯 JS 版本,用于概括一下:

export function selectJsonFilePureJS() {
    return new Promise((resolve, reject) => {
        let input = document.createElement("input");
        input.setAttribute("type", "file");
        input.setAttribute("accept", ".json");
        input.style.display = "none";
        input.onchange = () => {
            let file = input.files[0];
            if (file)
                resolve(file);
            else
                reject();
            input.remove();
        };
        document.body.appendChild(input);
        input.click();
    });
}

现在,有没有真正好的处理方法?我找不到。

即使在 2018 年,也没有办法知道用户是否点击了 "cancel",除了跟踪您是否看到用户事件源自您页面上的任何地方,而对话框应该是打开的。

当文件对话框打开时,页面应该完全处于非活动状态,因此设置一些东西来指示当用户单击文件输入元素时对话框打开,然后监视一组详尽的用户输入事件,这些事件应该对话框打开时不可能。如果该监视器看到任何事件,那么您就知道用户实际上 而不是 查看文件对话框,因此我们可以推断他们取消了该对话框。届时,您可以触发 "file dialog got cancelled" 处理函数,并取消绑定事件监视器。

让我们编写其中一个监视器:

class EventMonitor {
  constructor(dialogWasCancelled) {
    this.dialogWasCancelled = dialogWasCancelled;
    // don't use a class function here, since we need to
    // unregister the exact function handle later, and we
    // don't get those if we use e=>this.x() for listening.
    this.x = (function() {
      this.dialogWasCancelled();
      this.unregister();
    }).bind(this);
    this.register();
  }
  register() {
    document.addEventListener("mousemove", this.x);
    document.addEventListener("touchstart", this.x);
    document.addEventListener("keypress", this.x);
    window.addEventListener("scroll", this.x);
  }
  unregister() {
    document.removeEventListener("mousemove", this.x);
    document.removeEventListener("touchstart", this.x);
    document.removeEventListener("keypress", this.x);
    window.removeEventListener("scroll", this.x);
  }
}

虽然 body focus 事件不是超级可靠,但 mousedowntouchstartscrollkeypress 事件是。当您看到其中任何一个时,您就知道用户正在查看您的页面,而不是应用程序文件对话框。你想要所有这四个,因为你可能有传统的桌面或 touch/mobile 用户,即使对于传统的桌面用户,也有很多人出于各种充分的理由只能使用键盘。最后,您还需要检查滚动,因为取决于输入设备,因为这是最常见的用户交互,可能根本没有任何鼠标、触摸或按键事件。

有了这个,我们可以可靠地监视页面以确定文件是否被选择,或者用户是否取消了对话框:

let monitor = false;

function handleFiles(evt) {
  // some browsers _will_ kick in a change event if
  // you had a file, then "cancel"ed it on a second
  // select. Chrome, for instance, considers that
  // an action that empties the selection again.
  let files = evt.target.files;
  if (files.length === 0) return dialogCancelled(evt);
  console.log("user selected a file");
  monitor = monitor.unregister();
}

function dialogCancelled(evt) {
  console.log("user did not select a file");
  monitor = monitor.unregister();
}

let fileInput = document.querySelector("input[type=file]")

fileInput.addEventListener("change", e => handleFiles(e));
fileInput.addEventListener("click", e => (monitor = new EventMonitor(dialogCancelled)));

您可以在 http://jsbin.com/timafokisu/edit?js,console,output

上实时测试该代码