如何检测文件输入框上的取消按钮是否被点击?

131

如何检测用户在html文件输入中取消文件输入?

onChange可以检测到选择文件时的操作,但我也想知道当用户取消选择(关闭文件选择对话框而没有选择任何内容)时的操作。


您还可以检测是否有人选择了文件,然后尝试选择发送一个但取消了,您将得到空的 e.target.files - jcubic
请问您能否接受一个答案呢? - 13garth
37个回答

50

虽然不是一个直接的解决方案,而且只能(据我测试)在onfocus事件上工作(需要相当限制性的事件阻止),但您可以通过以下方式实现:

document.body.onfocus = function(){ /*rock it*/ }

这个方法的好处是你可以在文件事件中随时附加/分离它,而且它似乎也能很好地处理隐藏输入(如果你使用可视化解决方案来替代糟糕的默认input type='file',那么这绝对是一个优点)。然后,你只需要确定输入值是否已更改。

示例:

var godzilla = document.getElementById('godzilla')

godzilla.onclick = charge

function charge()
{
    document.body.onfocus = roar
    console.log('chargin')
}

function roar()
{
    if(godzilla.value.length) alert('ROAR! FILES!')
    else alert('*empty wheeze*')
    document.body.onfocus = null
    console.log('depleted')
}

在这里查看它的运行效果:http://jsfiddle.net/Shiboe/yuK3r/6/

可惜,它似乎只能在Webkit浏览器上运行。也许其他人可以想出Firefox/IE的解决方案。


我不想让焦点事件每次都触发,所以我想使用addEventListener("focus",fn,{once: true})。但是,我无法使用document.body.addEventListener触发它。我不知道为什么。相反,我使用window.addEventListener得到了相同的结果。 - WoodenKitty
9
在Firefox和Edge上,这对我不起作用。然而,在window上监听focus事件之外,另外监听window.document上的mousemove事件似乎在任何地方都有效(至少在现代浏览器中是这样,我不关心过去十年的崩溃)。基本上,任何交互事件都可以用于此任务,该任务被打开的文件对话框所阻止。 - John Weisz
@WoodenKitty,你是如何使用addEventListener实现这个的?我正在尝试在Angular中做到这一点,但document.body对我也不起作用。 - Terrance Jackson
这个回复非常有帮助。我使用了一个修改版,基于 window.addEventListener('focus', cancelDetector),在处理其他监听器时是非破坏性的。 - DenverCoder9
Shiboe和@WoodenKitty,<body>元素没有'focus'或'blur'事件,并且onfocus / onblur方法仅用于兼容性。请使用window.addEventListener('focus', ...)。 - Codesmith
3
@JohnWeisz你的评论应该是THE答案。天才。 - Emperor Eto

25

我想要回答这个问题,因为我有一个新的解决方案。我有一个渐进式Web应用程序,允许用户拍摄照片和视频并上传它们。我们尽可能使用WebRTC,但对于支持较少的设备 *cough Safari cough* ,则会退回到HTML5文件选择器。 如果你正在开发一个特定于Android / iOS移动Web应用程序,该应用程序使用本机相机直接拍摄照片/视频,则这是我找到的最佳解决方案。

这个问题的关键在于,当页面加载时,filenull,但是当用户打开对话框并按下“取消”时,file仍然是null,因此它没有“更改”,因此不会触发“更改”事件。对于桌面设备,这并不太糟糕,因为大多数桌面UI不依赖于知道何时调用取消,但弹出相机来捕获照片/视频的移动UI非常依赖于知道何时按下取消。

我最初使用document.body.onfocus事件来检测用户从文件选择器返回的时间,并且对大多数设备都有效,但是iOS 11.3破坏了这一点,因为该事件未被触发。

概念

我的解决方案是*颤抖*通过测量CPU时间来确定页面当前处于前台还是后台。在移动设备上,处理时间给予当前在前台的应用程序。当相机可见时,它将窃取CPU时间并使浏览器优先级降低。我们所需要做的就是测量我们的页面得到了多少处理时间,当相机启动时,我们可用的时间会急剧下降。当相机被取消或关闭时,我们的可用时间会反弹回来。

实现

我们可以使用setTimeout()来在X毫秒后调用回调函数,并测量实际调用它所花费的时间来测量CPU时间。浏览器永远不会完全在X毫秒后调用它,但如果它足够接近,则必须处于前台。如果浏览器与请求的时间相差很大(超过10倍),则必须处于后台。这个基本实现如下:

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 />元素的属性来调用本机相机。

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

这个方法的效果比我预期的要好得多。它通过请求在 25 毫秒后调用一个计时器来运行 10 次试验。然后测量实际调用所花费的时间,如果 10 次试验的平均值小于 50 毫秒,我们就认为必须处于前台并且相机已经关闭了。如果大于 50 毫秒,则仍然必须处于后台并继续等待。

一些额外的细节

我使用了 setTimeout() 而不是 setInterval(),因为后者可以排队多个调用,这些调用立即相互执行。这可能会极大地增加我们数据中的噪声,因此即使它稍微复杂一些,我还是坚持使用 setTimeout()

这些特定数字对我效果很好,尽管我曾经见过相机取消操作被错误地检测到一次。我相信这是因为相机可能打开速度较慢,并且设备可能在实际变成后台之前运行了 10 次试验。添加更多的试验或在启动此函数之前等待 25-50 毫秒可能是解决此问题的方法。

桌面

不幸的是,这对桌面浏览器并不起作用。理论上,相同的技巧是可能的,因为它们确实将当前页面置于后台页面之上。然而,即使在背景下,许多桌面设备也有足够的资源以保持页面以全速运行,因此这个策略实际上并不起作用。

备选方案

很少有人提到我曾经探索过的一种备选方案——模拟 FileList。我们从 null 开始在 <input /> 中,如果用户打开相机并取消操作,则返回到 null,这不是变化,也不会触发任何事件。一种解决方法是在页面启动时将一个虚拟文件分配给 <input />,因此将其设置为 null 将是一种变化,这将触发适当的事件。

不幸的是,没有官方方法来创建 FileList,并且 <input /> 元素需要特定的 FileList,除了 null 之外不接受任何其他值。自然,由于一些旧的安全问题(显然已经不相关了),FileList 对象不能直接构造。在 <input /> 元素之外获取一个 FileList 的唯一方法是利用一种 hack,将数据复制粘贴到虚拟剪贴板事件中,该事件可以包含一个 FileList 对象(实际上是模拟了将文件拖放到您的网站上的事件)。这在 Firefox 中是可能的,但对于 iOS Safari 则不可行,因此对于我特定的使用情况来说不可行。

浏览器,请……

不用说,这显然荒谬。网页没有收到任何关于关键 UI 元素已更改的通知简直令人发笑。从规范上讲,这确实是一个 bug,因为


这仅仅表明文件输入字段处理(事件)缺少明显的功能。 - Antoniossss

22

无法实现。

文件对话框的结果不会暴露给浏览器。


能否检查文件选择器是否已打开? - Ppp
2
@Ppp - 不,浏览器不会告诉你那个。 - Oded
8
如果通过选择文件来关闭对话框,那么文件对话框的结果会被传递给浏览器,但如果没有选择文件,则不会。 - Trevor
4
使用文件对话框触发的更改会暴露给浏览器。如果没有选择文件,并且之后也没有选择文件,则表示没有更改,因此不会触发任何“change”事件。 - John Weisz
2
此外,如果已选择一个文件,并再次选择相同的文件,则也不会为其分派“更改”事件。 - Patrick Roberts
1
我猜最好的方法是在“点击”时清除文件输入,然后在“更改”时预览。以防万一。 - vintprox

8

对我大多数解决方案都不起作用。

问题在于你永远不知道哪个事件会首先触发,是click还是change?你不能假设任何顺序,因为它可能取决于浏览器的实现。

至少在Opera和Chrome(2015年后期),click会在填充文件之前触发,所以你永远不会知道files.length != 0的长度,直到你延迟clickchange之后触发。

以下是代码:

var inputfile = $("#yourid");

inputfile.on("change click", function(ev){
    if (ev.originalEvent != null){
        console.log("OK clicked");
    }
    document.body.onfocus = function(){
        document.body.onfocus = null;
        setTimeout(function(){
            if (inputfile.val().length === 0) console.log("Cancel clicked");
        }, 1000);
    };
});

如果您点击页面... "取消已点击"... =|(在Chrome中测试过) - Eduardo Lucio

8
当你选择一个文件并点击打开/取消时,input元素应该失去焦点 (即blur)。假设input的初始值为空,在你的blur处理程序中任何非空值都表示OK,而空值则表示取消。
更新:当input被隐藏时,无法触发blur。因此,在基于IFRAME的上传中无法使用此技巧,除非你想暂时显示input

3
在 Firefox 10 beta 和 Chrome 16 中,选择文件时不会触发 blur。我没有在其它浏览器中尝试过。 - Blaise
2
这不起作用 - 输入模糊效果会在单击后立即触发。Chrome 63.0.3239.84 x64 在 Win7 上。 - Qwertiy

8

新的文件系统访问API将再次使我们的生活变得容易 :)

try {
    const [fileHandle] = await window.showOpenFilePicker();
    const file = await fileHandle.getFile();
    // ...
}
catch (e) {
    console.log('Cancelled, no file selected');
}

浏览器支持非常有限(截至2021年1月)。示例代码在Chrome桌面版86中工作正常。


7
/* Tested on Google Chrome */
$("input[type=file]").bind("change", function() {
    var selected_file_name = $(this).val();
    if ( selected_file_name.length > 0 ) {
        /* Some file selected */
    }
    else {
        /* No file selected or cancel/close
           dialog button clicked */
        /* If user has select a file before,
           when they submit, it will treated as
           no file selected */
    }
});

7
你检查过你的else语句是否被执行过了吗? - jayarjo
据我所记,当我写这个答案的时候,如果用户选择了一个文件然后重新选择文件(但是取消了),它将被视为未选择任何文件,我只使用警报来测试所选文件名变量。不管怎样,自那以后我就没有再测试过了。 - Rizky
4
经过一些跨浏览器测试后,我可以说Firefox在取消操作时不会触发change事件,如果重新打开并再次点击取消,则也不会清除已选择的文件。有点奇怪,对吧? - mix3d

5

同样监听点击事件。

参考Shiboe的示例,这里提供一个jQuery示例:

var godzilla = $('#godzilla');
var godzillaBtn = $('#godzilla-btn');

godzillaBtn.on('click', function(){
    godzilla.trigger('click');
});

godzilla.on('change click', function(){

    if (godzilla.val() != '') {
        $('#state').html('You have chosen a Mech!');    
    } else {
        $('#state').html('Choose your Mech!');
    }

});

您可以在这里看到它的运行效果:http://jsfiddle.net/T3Vwz

2
我在Chrome 51上进行了测试,第一次选择文件时无法工作。解决方法是添加延迟:http://jsfiddle.net/7jfbmunh/ - Benoit Blanchon
您的小提琴在我这里无法运行 - 我使用的是Firefox 58.0.2。 - barrypicker

4

最简单的方法是检查临时内存中是否有任何文件。如果您想在用户每次点击文件输入时获得更改事件,可以触发它。

var yourFileInput = $("#yourFileInput");

yourFileInput.on('mouseup', function() {
    $(this).trigger("change");
}).on('change', function() {
    if (this.files.length) {
        //User chose a picture
    } else {
        //User clicked cancel
    }
});

4
如果您选择与之前相同的文件并单击“取消”,则可以捕获取消操作,方法如下:
<input type="file" id="myinputfile"/>
<script>
document.getElementById('myinputfile').addEventListener('change', myMethod, false);
function myMethod(evt) {
  var files = evt.target.files; 
  f= files[0];
  if (f==undefined) {
     // the user has clicked on cancel
  }
  else if (f.name.match(".*\.jpg")|| f.name.match(".*\.png")) {
     //.... the user has choosen an image file
     var reader = new FileReader();
     reader.onload = function(evt) { 
        try {
        myimage.src=evt.target.result;
            ...
         } catch (err) {
            ...
         }
     };
  }     
  reader.readAsDataURL(f);          
</script>

1
谢谢 @loveJs,这篇文章真的帮了我很多。 - VPK
4
不完全正确,如果用户选择相同的文件,则不是取消操作。=(原意不变,通俗易懂) - Tiago Peres França
13
onchange 只在发生变化时触发。因此,如果文件与之前的文件相同,则 onchange 监听器不会触发。 - Shane
1
在检测到第一次更改后,您可以清除输入选择(input.value =''),这样即使用户再次选择相同的文件,第二次 onchange 也会触发。 - Chris
@Chris,你不能操纵文件类型输入的输入值。如果你能从JavaScript中这样做,那将是一个安全问题。 - CodeGems
1
@CodeGems 可以通过编程方式将 input.value 设置为空字符串。但是您说得对,将其设置为任何其他值都是不允许的。 - Chris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接