以编程方式生成/激活的文件输入并不总是触发 `input` 事件

Posted

技术标签:

【中文标题】以编程方式生成/激活的文件输入并不总是触发 `input` 事件【英文标题】:Programmatically generated/activated file input doesn't always fire `input` event 【发布时间】:2019-02-05 18:24:40 【问题描述】:

我的 Web 应用程序上有一个按钮,在 click 事件处理程序中有以下代码:

const fileInputEl = document.createElement('input');
fileInputEl.type = 'file';
fileInputEl.accept = 'image/*';

fileInputEl.addEventListener('input', (e) => 
  if (!e.target.files.length) 
    return;
  

  // Handle files here...
);  

fileInputEl.dispatchEvent(new MouseEvent('click'));

有时(大约八分之一),在选择文件后,input 事件在选择文件后不会触发。我猜这是围绕元素生命周期的浏览器错误。

除了将元素附加到页面并稍后将其删除之外,还有什么方法可以解决?如今,在现代浏览器中处理此问题的正确方法是什么?

我正在 Windows 上使用 Google Chrome 进行测试。

JSFiddle:http://jsfiddle.net/pja1d5om/2/

【问题讨论】:

为什么使用input 事件进行文件输入? change 不是更合适吗? 似乎在 Firefox 上运行良好 Windows 10 上的 Google chrome 69.0.3497.92,每次选择文件时都会触发该事件。 @MunimMunna Weird...感谢您的测试!我在 android 上的 Chrome 也遇到了这个问题,想想看。我想知道这是否是最近修复的错误。我已经遇到这个问题至少 3 或 4 年了,但是在今天下午重新启动并获得 Chrome 更新后我无法重现它。 (无论哪个版本更改了选项卡的 UI。我现在使用的是 v69.0.3497.100。)如果在我为此添加赏金的那一天问题得到解决,那将是奇怪的巧合。多次访问页面或使用 JSFiddle 可能会产生副作用。 有趣,在 chrome for mac 上似乎还可以,只是连续选择了 18 次文件而没有看到错误。 【参考方案1】:

引用您的问题: 有时(大约 8 个中的 1 个),在选择文件后,输入事件在选择文件后不会触发。 p>

我可以使用 input 和使用 Google Chrome browser engine "Blink" 的 Opera(版本 55.0.2994.61,目前最新版本)使用 change 事件来确认此行为。大约 25 人中有 1 人会发生这种情况。

解决方案

发生这种情况是因为有时您的输入元素对象在文件对话框关闭后被删除,因为它不再使用。而当它发生时,您还没有可以接收inputchange 事件的目标。

要解决这个问题,只需在创建为隐藏对象后将输入元素添加到 DOM 中,如下所示:

fileInputEl.style.display = 'none';
document.body.appendChild(fileInputEl);

然后当事件被触发时,你可以像下面这样删除它:

document.body.removeChild(fileInputEl);

完整示例

function selectFile()

    var fileInputEl = document.createElement('input');
    fileInputEl.type = 'file';
    fileInputEl.accept = 'image/*';
    //on this way you can see how many files you select (is for test only):
    fileInputEl.multiple = 'multiple';

    fileInputEl.style.display = 'none';
    document.body.appendChild(fileInputEl);

    fileInputEl.addEventListener('input', function(e)
    
        // Handle files here...
        console.log('You have selected ' + fileInputEl.files.length + ' file(s).');
        document.body.removeChild(fileInputEl);
    );  

    try
    
        fileInputEl.dispatchEvent(new MouseEvent('click'));
    
    catch(e)
    
        console.log('Mouse Event error:\n' + e.message);
        // TODO:
        //Creating and firing synthetic events in IE/MS Edge:
        //https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/dn905219(v=vs.85)
    
<input type="button" onclick="selectFile()" value="Select file">

引用您的赏金说明: 赏金将颁发给......展示适当解决方法的人。

我以前建议的解决方法(现在无关紧要)

我们可以使用setInterval 函数来检查输入值是否被改变。我们将intervalID 保存在新的fileInputEl 中作为属性。因为我们总是创建一个新的文件输入元素,所以它的值在开始时总是空的(每次单击按钮时)。如果这个值发生了变化,我们可以在将它与空字符串进行比较时检测到它。当它发生时,我们将 fileInputEl 传递给 fileInputChanged() 函数并清除/停止我们的区间函数。

function selectFile()

    var fileInputEl = document.createElement('input');
    fileInputEl.type = 'file';
    fileInputEl.accept = 'image/*';
    //on this way you can see how many files you select (is for test only):
    fileInputEl.multiple = 'multiple';

    fileInputEl.intervalID = setInterval(function()
    
        // because we always create a new file input element then
        // its value is always empty, but if not then it was changed:
        if(fileInputEl.value != '')
            fileInputChanged(fileInputEl);
    , 100);

    try
    
        fileInputEl.dispatchEvent(new MouseEvent('click'));
    
    catch(e)
    
        console.log('Mouse Event error:\n' + e.message);
        // TODO:
        //Creating and firing synthetic events in IE/MS Edge:
        //https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/dn905219(v=vs.85)
    


function fileInputChanged(obj)

    // Handle files here...
    console.log('You have selected ' + obj.files.length + ' file(s).');
    clearInterval(obj.intervalID);
<input type="button" onclick="selectFile()" value="Select file">

【讨论】:

我很高兴听到您能够重现该问题!但是,我怀疑 setInterval 解决方案不是最佳的。如果这行得通,那么我们对这种行为有几个可能的解释。其中之一是不收集对输入元素的引用(因为它在正在进行的计时器中被引用),这允许稍后在其上触发事件。 (我的怀疑是输入元素被提前收集,尽管附加了事件处理程序。)另一个可能的解释是事件监听器直到稍后才绑定,这对我来说似乎是一个错误。 (续)我希望熟悉浏览器内部结构的人能够解释其中哪些是正确的,或者是否有其他解释。 我没有...我刚刚获得了赏金? @Brad,我已将其删除以检查谁删除了他的赞成票,然后又否决了我的回答。从我的回答中删除他的声誉得到 +1 后,我现在从他的声誉中认出了这一点。我现在知道是谁了,当他取消他的反对票并再次支持我的回答时,我会原谅他。我不明白为什么有人在 SO 上否决了好的答案。我从来没有对 SO 最糟糕的答案投反对票——只有糟糕的问题。我的答案写得非常好(解决方案+解决方法),投反对票是不公平的。 @Brad,我还在等你的答复。承认错误是力量而不是弱点。在您纠正错误后,我们可以忘记它。 ;-)【参考方案2】:

这是一个非常有趣的错误,我无法重现它。

您以您的方式处理文件输入是否有原因?是因为你想尝试和设计它吗?

我阅读了this article,并应用了它正在做的事情。我发现这很好用。本文试图做的是将一个标签连接到输入,并在标签标签上添加一个 for 属性。然后在 CSS 和 javascript 中,文件输入标签是隐藏的,标签本质上是“按钮”。

例如...

注意我确实对代码进行了一些更改,但所有这些都归功于 CoDrops 上面提到的文章的作者 Osvaldas Valutis。

var inputs = document.querySelectorAll('.inputfile');

inputs.forEach(input => 

  var label = input.nextElementSibling,
    labelVal = label.innerhtml;

  input.addEventListener('change', function(e) 

    var fileName = '';

    if (this.files && this.files.length > 1)
      fileName = (this.getAttribute('data-multiple-caption') || '').replace('count', this.files.length);
    else
      fileName = e.target.value.split('\\').pop();

    if (fileName)
      label.querySelector('span').innerHTML = fileName;
    else
      label.innerHTML = labelVal;

  );

);
* 
  font-family: sans-serif;
  font-weight: 300;


.inputfile 
  display: none;


.inputfile+label 
  font-size: 1.25em;
  font-weight: 700;
  color: white;
  background-color: darkred;
  display: inline-block;
  padding: 10px;
  border-radius: 10px;
  border: 1px darkred solid;
  cursor: pointer;


.inputfile+label:hover 
  background-color: darkred;
<input type="file" name="file" id="file" class="inputfile" data-multiple-caption="count files selected" multiple />
<label for="file">Choose a file <span></span></label>

现在我知道这可能不是您正在寻找的,但这可能是您可以尝试的替代解决方案。

【讨论】:

【参考方案3】:

这似乎是一个浏览器错误/侥幸,可能与垃圾收集有关。我可以通过将文件输入添加到文档来解决它:

fileInputEl.style.display = 'none';
document.querySelector('body').appendChild(fileInputEl);

完成后,可以使用以下命令进行清理:

fileInputEl.remove();

【讨论】:

以上是关于以编程方式生成/激活的文件输入并不总是触发 `input` 事件的主要内容,如果未能解决你的问题,请参考以下文章

如何以编程方式生成按键事件? [复制]

当输入的值以编程方式更改时触发更改事件

以编程方式触发 iOS 抖动事件

以编程方式触发 typeahead.js 结果显示

为啥我的Myeclipse9.0总是激活失败

Application_Error 并不总是触发 ASP.NET