如何检测何时单击文件输入取消? [英] How to detect when cancel is clicked on file input?

查看:25
本文介绍了如何检测何时单击文件输入取消?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何检测用户何时使用 html 文件输入取消文件输入?

How can I detect when the user cancels a file input using an html file input?

onChange 让我检测他们何时选择文件,但我也想知道他们何时取消(关闭文件选择对话框而不选择任何内容).

onChange lets me detect when they choose a file, but I would also like to know when they cancel (close the file choose dialog without selecting anything).

推荐答案

因此,由于我想出了一个新颖的解决方案,所以我会毫不犹豫地回答这个问题.我有一个渐进式 Web 应用程序,它允许用户捕捉照片和视频并上传它们.我们在可能的情况下使用 WebRTC,但对于支持较少 *cough Safari 咳嗽 * 的设备,我们会回退到 HTML5 文件选择器.如果您专门开发使用本机相机直接捕捉照片/视频的 Android/iOS 移动网络应用程序,那么这是我遇到的最佳解决方案.

So I'll throw my hat into this question since I came up with a novel solution. I have a Progressive Web App which allows users to capture photos and videos and upload them. We use WebRTC when possible, but fall back to HTML5 file pickers for devices with less support *cough Safari cough*. If you're working specifically on an Android/iOS mobile web application which uses the native camera to capture photos/videos directly, then this is the best solution I have come across.

这个问题的症结在于,当页面加载时,filenull,但是当用户打开对话框并按下取消"时,file 仍然是 null,因此它没有更改",因此不会触发更改"事件.对于桌面设备,这还不错,因为大多数桌面 UI 不依赖于知道何时调用取消,但是启动相机以捕获照片/视频的移动 UI非常依赖于知道何时按下取消.

The crux of this problem is that when the page loads, the file is null, but then when the user opens the dialog and presses "Cancel", the file is still null, hence it did not "change", so no "change" event is triggered. For desktops, this isn't too bad because most desktop UI's aren't dependent on knowing when a cancel is invoked, but mobile UI's which bring up the camera to capture a photo/video are very dependent on knowing when a cancel is pressed.

我最初使用 document.body.onfocus 事件来检测用户何时从文件选择器返回,这适用于大多数设备,但 iOS 11.3 由于未触发该事件而破坏了它.

I originally used the document.body.onfocus event to detect when the user returned from the file picker, and this worked for most devices, but iOS 11.3 broke it as that event is not triggered.

我对此的解决方案是 *shudder* 测量 CPU 时间以确定页面当前是在前台还是在后台.在移动设备上,处理时间分配给当前处于前台的应用程序.当摄像头可见时,它会窃取 CPU 时间并降低浏览器的优先级.我们需要做的就是衡量我们的页面有多少处理时间,当相机启动时,我们的可用时间将急剧下降.当相机关闭(取消或取消)时,我们的可用时间会重新增加.

My solution to this is *shudder* to measure CPU timing to determine if the page is currently in the foreground or the background. On mobile devices, processing time is given to the app currently in the foreground. When a camera is visible it will steal CPU time and deprioritize the browser. All we need to do is measure how much processing time our page is given, when camera launches our available time will drop drastically. When the camera is dismissed (either cancelled or otherwise), our available time spike back up.

我们可以通过使用 setTimeout() 在 X 毫秒内调用回调来测量 CPU 时序,然后测量实际调用它所花费的时间.浏览器永远不会在 X 毫秒后完全调用它,但如果它合理关闭,那么我们必须在前台.如果浏览器距离很远(比请求慢 10 倍以上),那么我们必须在后台.一个基本的实现是这样的:

We can measure CPU timing by using setTimeout() to invoke a callback in X milliseconds, and then measure how long it took to actually invoke it. The browser will never invoke it exactly after X milliseconds, but if it is reasonable close then we must be in the foreground. If the browser is very far away (over 10x slower than requested) then we must be in the background. A basic implementation of this is like so:

function waitForCameraDismiss() {
  const REQUESTED_DELAY_MS = 25;
  const ALLOWED_MARGIN_OF_ERROR_MS = 25;
  const MAX_REASONABLE_DELAY_MS =
      REQUESTED_DELAY_MS + ALLOWED_MARGIN_OF_ERROR_MS;
  const MAX_TRIALS_TO_RECORD = 10;

  const triggerDelays = [];
  let lastTriggerTime = Date.now();

  return new Promise((resolve) => {
    const evtTimer = () => {
      // Add the time since the last run
      const now = Date.now();
      triggerDelays.push(now - lastTriggerTime);
      lastTriggerTime = now;

      // Wait until we have enough trials before interpreting them.
      if (triggerDelays.length < MAX_TRIALS_TO_RECORD) {
        window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
        return;
      }

      // Only maintain the last few event delays as trials so as not
      // to penalize a long time in the camera and to avoid exploding
      // memory.
      if (triggerDelays.length > MAX_TRIALS_TO_RECORD) {
        triggerDelays.shift();
      }

      // Compute the average of all trials. If it is outside the
      // acceptable margin of error, then the user must have the
      // camera open. If it is within the margin of error, then the
      // user must have dismissed the camera and returned to the page.
      const averageDelay =
          triggerDelays.reduce((l, r) => l + r) / triggerDelays.length
      if (averageDelay < MAX_REASONABLE_DELAY_MS) {
        // Beyond any reasonable doubt, the user has returned from the
        // camera
        resolve();
      } else {
        // Probably not returned from camera, run another trial.
        window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
      }
    };
    window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
  });
}

我在最新版本的 iOS 和 Android 上对此进行了测试,通过在 <input/> 元素上设置属性来启动本机相机.

I tested this on recent version of iOS and Android, bringing up the native camera by setting the attributes on the <input /> element.

<input type="file" accept="image/*" capture="camera" />
<input type="file" accept="video/*" capture="camcorder" />

这实际上比我预期的要好得多.它通过请求在 25 毫秒内调用一个计时器来运行 10 次试验.然后它测量调用实际花费的时间,如果 10 次试验的平均值小于 50 毫秒,我们假设我们必须在前台并且相机不见了.如果大于50毫秒,那么我们肯定还在后台,应该继续等待.

This works out actually a lot better than I expected. It runs 10 trials by requesting a timer to be invoked in 25 milliseconds. It then measures how long it actually took to invoke, and if the average of 10 trials is less than 50 milliseconds, we assume that we must be in the foreground and the camera is gone. If it is greater than 50 milliseconds, then we must still be in the background and should continue to wait.

我使用了 setTimeout() 而不是 setInterval() 因为后者可以将多个调用排入队列,这些调用会在彼此之后立即执行.这可能会大大增加我们数据中的噪音,所以我坚持使用 setTimeout(),尽管这样做有点复杂.

I used setTimeout() rather than setInterval() because the latter can queue multiple invocations which execute immediately after each other. This could drastically increase the noise in our data, so I stuck with setTimeout() even though it is a little more complicated to do so.

这些特定数字对我来说效果很好,但我至少见过一次过早检测到相机关闭的实例.我相信这是因为相机打开速度可能很慢,并且设备可能会在实际变为后台之前运行 10 次试验.在启动此功能之前添加更多试验或等待大约 25-50 毫秒可能是一种解决方法.

These particular numbers worked well for me, though I have see at least once instance where the camera dismiss was detected prematurely. I believe this is because the camera may be slow to open, and the device may run 10 trials before it actually becomes backgrounded. Adding more trials or waiting some 25-50 milliseconds before starting this function may be a workaround for that.

不幸的是,这不适用于桌面浏览器.理论上,同样的技巧是可能的,因为它们确实优先考虑当前页面而不是背景页面.然而,许多桌面有足够的资源来保持页面全速运行,即使在后台运行时,这种策略在实践中并没有真正奏效.

Unfortuantely, this doesn't really work for desktop browsers. In theory the same trick is possible as they do prioritize the current page over backgrounded pages. However many desktops have enough resources to keep the page running at full speed even when backgrounded, so this strategy doesn't really work in practice.

一个没有多少人提到我探索过的替代解决方案是模拟 FileList.我们从 <input/> 中的 null 开始,然后如果用户打开相机并取消他们回到 null,不是更改,也不会触发任何事件.一种解决方案是在页面开始时为 分配一个虚拟文件,因此设置为 null 将是一个会触发适当事件的更改.

One alternative solution not many people mention that I did explore was mocking a FileList. We start with null in the <input /> and then if the user opens the camera and cancels they come back to null, which is not a change and no event will trigger. One solution would be to assign a dummy file to the <input /> at page start, therefore setting to null would be a change which would trigger the appropriate event.

不幸的是,没有办法创建FileList,而且<input/> 元素特别需要FileList并且不会接受除 null 之外的任何其他值.自然地,FileList 对象不能直接构造,解决一些显然不再相关的旧安全问题.在 <input/> 元素之外获得一个的唯一方法是利用一个黑客复制粘贴数据来伪造一个剪贴板事件,该事件可以包含一个 FileList 对象(您基本上是在伪造一个在您的网站上拖放文件的事件).这在 Firefox 中是可能的,但不适用于 iOS Safari,因此它不适用于我的特定用例.

Unfortunately, there's no way official way to create a FileList, and the <input /> element requires a FileList in particular and will not accept any other value besides null. Naturally, FileList objects cannot be directly constructed, do to some old security issue which isn't even relevant anymore apparently. The only way to get ahold of one outside of an <input /> element is to utilize a hack which copy-pastes data to fake a clipboard event which can contain a FileList object (you're basically faking a drag-and-drop-a-file-on-your-website event). This is possible in Firefox, but not for iOS Safari, so it was not viable for my particular use case.

不用说,这显然是荒谬的.网页在关键 UI 元素已更改时收到零通知这一事实简直可笑.这确实是规范中的一个错误,因为它从未打算用于全屏媒体捕获 UI,并且不触发更改"事件技术上符合规范.

Needless to say this is patently ridiculous. The fact that web pages are given zero notification that a critical UI element has changed is simply laughable. This is really a bug in the spec, as it was never intended for a full-screen media capture UI, and not triggering the "change" event is technically to spec.

但是,浏览器厂商能否认识到这一点?这可以通过一个新的完成"事件来解决,即使没有发生任何更改也会触发该事件,或者您无论如何都可以触发更改".是的,这会违反规范,但是在 JavaScript 端删除更改事件对我来说是微不足道的,但从根本上不可能发明我自己的完成"事件.如果不能保证浏览器的状态,即使我的解决方案也只是启发式方法.

However, can browser vendors please recognize the reality of this? This could be solved with either a new "done" event which is triggered even when no change occurs, or you could just trigger "change" anyways. Yeah, that would be against spec, but it is trivial for me to dedup a change event on the JavaScript side, yet fundamentally impossible to invent my own "done" event. Even my solution is really just heuristics, if offer no guarantees on the state of the browser.

就目前而言,这个 API 从根本上无法用于移动设备,我认为一个相对简单的浏览器更改可以让网络开发人员更容易*离开肥皂盒*.

As it stands, this API is fundamentally unusable for mobile devices, and I think a relatively simple browser change could make this infinitely easier for web developers *steps off soap box*.

这篇关于如何检测何时单击文件输入取消?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆