如何检测用户在html文件输入中取消文件输入?
onChange可以检测到选择文件时的操作,但我也想知道当用户取消选择(关闭文件选择对话框而没有选择任何内容)时的操作。
虽然不是一个直接的解决方案,而且只能(据我测试)在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
得到了相同的结果。 - WoodenKittywindow
上监听focus
事件之外,另外监听window.document
上的mousemove
事件似乎在任何地方都有效(至少在现代浏览器中是这样,我不关心过去十年的崩溃)。基本上,任何交互事件都可以用于此任务,该任务被打开的文件对话框所阻止。 - John Weiszwindow.addEventListener('focus', cancelDetector)
,在处理其他监听器时是非破坏性的。 - DenverCoder9我想要回答这个问题,因为我有一个新的解决方案。我有一个渐进式Web应用程序,允许用户拍摄照片和视频并上传它们。我们尽可能使用WebRTC,但对于支持较少的设备 *cough Safari cough* ,则会退回到HTML5文件选择器。 如果你正在开发一个特定于Android / iOS移动Web应用程序,该应用程序使用本机相机直接拍摄照片/视频,则这是我找到的最佳解决方案。
这个问题的关键在于,当页面加载时,file
是null
,但是当用户打开对话框并按下“取消”时,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,因为
无法实现。
文件对话框的结果不会暴露给浏览器。
对我大多数解决方案都不起作用。
问题在于你永远不知道哪个事件会首先触发,是click
还是change
?你不能假设任何顺序,因为它可能取决于浏览器的实现。
至少在Opera和Chrome(2015年后期),click
会在填充文件之前触发,所以你永远不会知道files.length != 0
的长度,直到你延迟click
在change
之后触发。
以下是代码:
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);
};
});
input
元素应该失去焦点 (即blur
)。假设input
的初始值为空,在你的blur
处理程序中任何非空值都表示OK,而空值则表示取消。input
被隐藏时,无法触发blur
。因此,在基于IFRAME的上传中无法使用此技巧,除非你想暂时显示input
。blur
。我没有在其它浏览器中尝试过。 - Blaise新的文件系统访问API将再次使我们的生活变得容易 :)
try {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
// ...
}
catch (e) {
console.log('Cancelled, no file selected');
}
浏览器支持非常有限(截至2021年1月)。示例代码在Chrome桌面版86中工作正常。
/* 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 */
}
});
else
语句是否被执行过了吗? - jayarjo同样监听点击事件。
参考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!');
}
});
最简单的方法是检查临时内存中是否有任何文件。如果您想在用户每次点击文件输入时获得更改事件,可以触发它。
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
}
});
<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>
e.target.files
。 - jcubic