是否有一种 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
事件不是超级可靠,但 mousedown
、touchstart
、scroll
和 keypress
事件是。当您看到其中任何一个时,您就知道用户正在查看您的页面,而不是应用程序文件对话框。你想要所有这四个,因为你可能有传统的桌面或 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)));
上实时测试该代码
这是我的具体用例:我想点击文件对话框的 "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
事件不是超级可靠,但 mousedown
、touchstart
、scroll
和 keypress
事件是。当您看到其中任何一个时,您就知道用户正在查看您的页面,而不是应用程序文件对话框。你想要所有这四个,因为你可能有传统的桌面或 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)));
上实时测试该代码