Programmatically generated/activated file input doesn't always fire `input` event - javascript

I have a button on my web application, which has the following code in the click event handler:
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'));
Sometimes (about 1 out of 8), after selecting the file, the input event doesn't fire after choosing a file. I'm guessing this is a browser bug around the lifecycle of the element.
Any way around this short of appending the element to the page and removing it later? What's the proper way to handle this in modern browsers these days?
I'm testing with Google Chrome on Windows.
JSFiddle: http://jsfiddle.net/pja1d5om/2/

Citate from your question: Sometimes (about 1 out of 8), after selecting the file, the input event doesn't fire after choosing a file.
I can confirm this behavior with input and with change events using Opera (ver. 55.0.2994.61, newest version at this time) which uses Google Chrome browser engine "Blink". It happens about 1 out of 25.
Solution
This happens because sometimes your input element object was deleted after file dialog closing because it is not in using anymore. And when it happens you have not the target which could receive input or change event.
To solve this just add your input element somewhere to the DOM after creating as hidden object like follows:
fileInputEl.style.display = 'none';
document.body.appendChild(fileInputEl);
And then when the event was fired you can delete it like follows:
document.body.removeChild(fileInputEl);
Full example
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://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/dn905219(v=vs.85)
}
}
<input type="button" onclick="selectFile()" value="Select file">
Citate from your bounty description: Bounty will be awarded to someone who ... show an appropriate workaround.
My old suggested workaround (now irrelevant)
We can use setInterval function to check if input value was changed. We save intervalID in our new fileInputEl as property. Because we always create a new file input element then its value is always empty on start (on each button click). And if this value was changed we can detect it when we compare it with empty string. And when it happens then we pass our fileInputEl to fileInputChanged() function and clear/stop our interval function.
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://learn.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">

It seems this is a browser bug/fluke and likely has something to do with garbage collection. I can get around it by adding the file input to the document:
fileInputEl.style.display = 'none';
document.querySelector('body').appendChild(fileInputEl);
When done, it can be cleaned up with:
fileInputEl.remove();

Related

How to cancel / preventDefault drop event in CKEditor?

In CKEditor 4 application (working jsFiddle ) I do not want the default drop functionality to be executed in case "special" element is dropped. So I created a listener on "drop" event where I call a function editor.execCommand("nonEditableBlock" that I created. This function inserts a block. It also inserts text. I do not want the text to be inserted. I tried to insert "" or null but it causing other issues. So I thought I would quit the drop event. I tried
evt.stop(); // works only 1st time of dropping
evt.data.preventDefault() // not a function
evt.data.preventDefault(true) // not a function
evt.preventDefault() // not a function
evt.cancel() // works only 1st time
return false // works only 1st time
evt.stopPropagation() // not a function
evt.data.stopPropagation() // not a function
Would you know how to prevent the default or how to not to paste the dragged text?
CKEDITOR.on('instanceReady', function(ev) {
ev.editor.on('drop', function (evt) {
console.log('Drop started')
var timestamp = " [["+new Date().toLocaleString().replace(',','')+"]]"
var data = evt.data.dataTransfer.getData('block')
if (data){
var dataTransfer = evt.data.dataTransfer;
// dataTransfer.setData( 'text/html', null ); //set data
dataTransfer.setData( 'text/html', "xxx "+timestamp ); //set data
console.log("nulling data",evt)
console.log("nulling data",evt.data)
}
console.log("testing drop 1")
editor.execCommand("nonEditableBlock", {startupData: {
id: data.data("id"),
content: data.data("data"),
timestamp:timestamp,
}});
console.log("testing drop 2")
// Stop execute next callbacks.
//evt.stop(); // works only 1st time
// Stop the default event action.
// evt.data.preventDefault() // not a function
// evt.data.preventDefault(true) // not a function
// evt.preventDefault() // not a function
// evt.cancel() // works only 1st time
//evt.stopPropagation() // not a function
//evt.data.stopPropagation() // not a function
console.log("testing drop 3")
//return false // works only 1st time
})
What I find confusing is that
if evt.cancel() is used then drop event is fired but editor.execCommand(" command is NOT executed. Although it is inside the event. See the console for debugging. On the first run the command is called so you can see nonEditableBlock widget called in the console.
if you create empty line by pressing enter then it works. In fact in the editor will be only blocks. Nothing except block will be allowed. No text, no empty lines.

<input type="file"> accept property will be ignored when drag and drop file, how can I prevent this?

I want to force the user to only select a CSV or excel file.
Please see this minimum example:
<input type="file" accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel">
Although I can use accept property to force the file selector not to select other files, I can still use drag and drop files directly from Finder, and it will works--the accept property will be ignored.
Is it possible to prevent this?
You will need to perform server-side validation, but you can make the user experience better by checking the type of the file against a Set of allowed types.
const allowedTypes = new Set(['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel']);
document.querySelector('input[type=file]').addEventListener('change', function(){
if(!allowedTypes.has(this.files[0].type)){
console.log("Not a CSV file");
this.value = '';//clear the input for invalid file
} else {
console.log("CSV file");
}
});
<input type="file" accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel">
Note that csv files has many MimeTypes so you should check for more than
"application/vnd.ms-excel" => .CSV Mimetypes, and you can check against that in client side
as well by comparing the type of the file against an array of your accepted types
that way you can add or delete the way that fits your needs
// the list of the accepted types since we need it always it's better to
// make it global instead of local to the onchange litener, and even you can
// add other types dynamically as well;
const acceptedTypes = ["text/csv", "text/x-csv", "application/x-csv", "application/csv", "text/x-comma-separated-values", "text/comma-separated-values", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-excel"];
document.querySelector("[type='file']").onchange = function() {
if(!acceptedTypes.includes(this.files[0].type)) {
console.log("This file is not allowed for upload");
// if the file is not allowed then clear the value of the upload element
this.value = "";
}
};
And if you want this behaviour only when the user drags and drops the file then you can customize it like this
const acceptedTypes = ["text/csv", "text/x-csv", "application/x-csv", "application/csv", "text/x-comma-separated-values", "text/comma-separated-values", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-excel"];
// global variable to hold if the user has dragged the file or not
let isDragged = false;
// the `ondragover` gets triggered before the `onchange` event so it works as expected
document.querySelector("[type='file']").ondragover = function() {
isDragged = true;
};
document.querySelector("[type='file']").onchange = function() {
// if there was no drag then do nothing
if(!isDragged) return;
if(!acceptedTypes.includes(this.files[0].type)) {
console.log("This file is not allowed for upload");
// if the file is not allowed then clear the value of the upload element
this.value = "";
}
isDragged = false;
};
You just have to add an event listener and function to either accept or reject the file:
fileInput.addEventListener("change", func);
var func = function() {
if(fileInput.files[0].type == /*insert your file types here*/) {
//accept file and do stuff with it
} else {
//reject and tell user that is was rejected and why
}
}
I am not sure this works with the drag & drop file input, but I know it works with the regular type. The change event is called whenever the file changes.

How to right click from javascript

It seems that it is not possible to emulate a right click in javascript.
I have tried to right click an element (paragraph) in an iframe like this:
html
<button onclick="popup_context_menu_in_iframe()">
popup menu
</button>
<br/><br/>
<iframe srcdoc="<p>Hello world!</p>">
</iframe>
script
function popup_context_menu_in_iframe()
{
var $element = $('iframe').contents().find('p');
var element = $element.get(0);
if (window.CustomEvent) {
element.dispatchEvent(new CustomEvent('contextmenu'));
} else if (document.createEvent) {
var ev = document.createEvent('HTMLEvents');
ev.initEvent('contextmenu', true, false);
element.dispatchEvent(ev);
} else { // Internet Explorer
element.fireEvent('oncontextmenu');
}
}
https://jsfiddle.net/sca60d64/2/
It seems like it actually is impossible to make the context menu appear so I need to find other ways.
I first headed at creating a chrome extension to add a function to the window object, callable from any script that is using some extra power to do it.
However, A chrome extension surprisingly seems to not provide me with a way of creating functions in the window object. I have not taken a look if it even gives me the power to popup the context menu.
I did not experiment a lot with chrome extensions before giving up on that.
So I need another solution.
It doesnt matter if a solution only works in google chrome or if there is no guarantee that it will stop work in the next version.
Should I hook the chrome process with a dll? Is that it?
You can call a dll by an exe file in the chrome extension through Native Messaging. I have provided a sample of Native Messaging procedure in this answer:
See the answer of this question
I hope to be useful.
This should work with this markup:
<div id="mything">
Howdy this is my thing
</div>
event handler:(disable default)
var el=document.getElementById("mything");
el.addEventListener('contextmenu', function(ev) {
ev.preventDefault();
alert('success!');
return false;
}, false);
Then when you execute this, "sucess!" alerts followed by the text changing to "Howdy this is my thing this is canceled":
EDIT event handler:(do NOT disable default)
var el=document.getElementById("mything");
el.addEventListener('contextmenu', function(ev) {
ev.preventDefault();
alert('success!');
return true;
}, true);
Then when you execute this, "sucess!" alerts followed by the text changing to "Howdy this is my thing this is NOT canceled":
function simulateRightClick() {
var event = new MouseEvent('contextmenu', {
'view': window,
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('mything');
var canceled = !cb.dispatchEvent(event);
var cbtext = cb.textContent;
if (canceled) {
// A handler called preventDefault.
console.log("canceled");
cb.textContent = cbtext + "this is canceled";
} else {
// None of the handlers called preventDefault.
cb.textContent = cbtext + "this is NOT canceled";
console.log("not canceled");
}
}
simulateRightClick();
Test it out here: https://jsfiddle.net/MarkSchultheiss/bp29s0j4/
EDIT: alternate selector:
var fcb = document.getElementById('myframe').contentWindow.document.getElementById('pid');
fcb.addEventListener('contextmenu', function(ev) {
ev.preventDefault();
alert('successFrame!');
return false;
}, false);
Given this markup:
<iframe id='myframe' srcdoc="<p id='pid'>Hello world!</p>">
</iframe>

Click all anchor tags on page with given class, but cancel prior to navigation

Trying to automate some testing for some analytics tracking code, and I'm running into issues when I try passing links into the each() method.
I copied a lot of this from stackoverflow - how to follow all links in casperjs, but I don't need return the href of the link; I need to return the link itself (so I can click it). I keep getting this error: each() only works with arrays. Am I not returning an array?
UPDATE:
For each anchor tag that has .myClass, click it, then return requested parameters from casper.options.onResourceReceived e.g. event category, event action, etc. I may or may not have to cancel the navigation the happens after the click; I simply only need to review the request, and do not need the follow page to load.
Testing steps:
click link that has .myClass
look at request parameters
cancel the click to prevent it from going to the next page.
I'm new to javascript and casper.js, so I apologize if I'm misinterpreting.
ANOTHER UPDATE:
I've updated the code to instead return an array of classes. There are a few sketchy bits of code in this though (see comments inline).
However, I'm now having issues canceling the navigation after the click. .Clear() canceled all js. Anyway to prevent default action happening after click? Like e.preventDefault();?
var casper = require('casper').create({
verbose: true,
logLevel: 'debug'
});
casper.options.onResourceReceived = function(arg1, response) {
if (response.url.indexOf('t=event') > -1) {
var query = decodeURI(response.url);
var data = query.split('&');
var result = {};
for (var i = 0; i < data.length; i++) {
var item = data[i].split('=');
result[item[0]] = item[1];
}
console.log('EVENT CATEGORY = ' + result.ec + '\n' +
'EVENT ACTION = ' + result.ea + '\n' +
'EVENT LABEL = ' + decodeURIComponent(result.el) + '\n' +
'REQUEST STATUS = ' + response.status
);
}
};
var links;
//var myClass = '.myClass';
casper.start('http://www.leupold.com', function getLinks() {
links = this.evaluate(function() {
var links = document.querySelectorAll('.myClass');
// having issues when I attempted to pass in myClass var.
links = Array.prototype.map.call(links, function(link) {
// seems like a sketchy way to get a class. what happens if there are multiple classes?
return link.getAttribute('class');
});
return links;
});
});
casper.waitForSelector('.myClass', function() {
this.echo('selector is here');
//this.echo(this.getCurrentUrl());
//this.echo(JSON.stringify(links));
this.each(links, function(self, link) {
self.echo('this is a class : ' + link);
// again this is horrible
self.click('.' + link);
});
});
casper.run(function() {
this.exit();
});
There are two problems that you're dealing with.
1. Select elements based on class
Usually a class is used multiple times. So when you first select elements based on this class, you will get elements that have that class, but it is not guaranteed that this will be unique. See for example this selection of element that you may select by .myClass:
myClass
myClass myClass2
myClass myClass3
myClass
myClass myClass3
When you later iterate over those class names, you've got a problem, because 4 and 5 can never be clicked using casper.click("." + links[i].replace(" ", ".")) (you need to additionally replace spaces with dots). casper.click only clicks the first occurrence of the specific selector. That is why I used createXPathFromElement taken from stijn de ryck to find the unique XPath expression for every element inside the page context.
You can then click the correct element via the unique XPath like this
casper.click(x(xpathFromPageContext[i]));
2. Cancelling navigation
This may depend on what your page actually is.
Note: I use the casper.test property which is the Tester module. You get access to it by invoking casper like this: casperjs test script.js.
Note: There is also the casper.waitForResource function. Have a look at it.
2.1 Web 1.0
When a click means a new page will be loaded, you may add an event handler to the page.resource.requested event. You can then abort() the request without resetting the page back to the startURL.
var resourceAborted = false;
casper.on('page.resource.requested', function(requestData, request){
if (requestData.url.match(/someURLMatching/)) {
// you can also check requestData.headers which is an array of objects:
// [{name: "header name", value: "some value"}]
casper.test.pass("resource passed");
} else {
casper.test.fail("resource failed");
}
if (requestData.url != startURL) {
request.abort();
}
resourceAborted = true;
});
and in the test flow:
casper.each(links, function(self, link){
self.thenClick(x(link));
self.waitFor(function check(){
return resourceAborted;
});
self.then(function(){
resourceAborted = false; // reset state
});
});
2.2 Single page application
There may be so many event handlers attached, that it is quite hard to prevent them all. An easier way (at least for me) is to
get all the unique element paths,
iterate over the list and do every time the following:
Open the original page again (basically a reset for every link)
do the click on the current XPath
This is basically what I do in this answer.
Since single page apps don't load pages. The navigation.requested and page.resource.requested will not be triggered. You need the resource.requested event if you want to check some API call:
var clickPassed = -1;
casper.on('resource.requested', function(requestData, request){
if (requestData.url.match(/someURLMatching/)) {
// you can also check requestData.headers which is an array of objects:
// [{name: "header name", value: "some value"}]
clickPassed = true;
} else {
clickPassed = false;
}
});
and in the test flow:
casper.each(links, function(self, link){
self.thenOpen(startURL);
self.thenClick(x(link));
self.waitFor(function check(){
return clickPassed !== -1;
}, function then(){
casper.test.assert(clickPassed);
clickPassed = -1;
}, function onTimeout(){
casper.test.fail("Resource timeout");
});
});

Removing a file in modern browsers

Problem
I am currently using ( https://github.com/blueimp/jQuery-File-Upload/wiki ) this jQuery HTML5 Uploader.
The basic version, no ui.
The big problem is, that I looked everywhere (Mozilla Developer Network, SO, Google, etc.) and found no solution for removing a files already added via dragNdrop or manually via the file input dialogue.
Why do I want to achieve removing a file?
Because it seems that HTML5 has a kind of "bug".
If you drop / select a file (file input has set multiple) upload it, and then drop / select another file you magically have now the new file twice and it gets uploaded twice.
To prevent this magic file caching the use would have to refresh the page, which is not what someone wants to have for his modern AJAX web app.
What I have tried so far:
.reset()
.remove()
Reset Button
Setting .val() to ''
This seems to be a general HTML5 JS problem not jQuery specific.
Theory
Might it be, that $j('#post').click (I bind / re-bind a lot of times different callbacks), stacks the callbacks methods so that each time the updateFileupload function is called an additional callback is set.
The actual problem would now not rely anymore on the HTML5 upload, it would now rely on my could, miss-binding the .click action on my submit button (id=#post).
If we now call .unbind before each .click there shouldn't be any duplicated callback binding.
Code
Function containing the upload code:
function updateFileupload (type) {
var destination = "";
switch(type)
{
case upload_type.file:
destination = '/wall/uploadfile/id/<?=$this->id?>';
break;
case upload_type.image:
destination = '/wall/upload/id/<?=$this->id?>';
break;
}
$j('#fileupload').fileupload({
dataType: 'json',
url: destination,
singleFileUploads: false,
autoUpload: false,
dropZone: $k(".dropZone"),
done: function (e, data) {
console.log("--:--");
console.log(data.result);
upload_result = data.result;
console.log(upload_result);
console.log("--:--");
console.log(type);
if(type == upload_type.image)
{
var imageName = upload_result.real;
console.log(imageName);
$k.get('/wall/addpicture/id/<?=$this->id ?>/name'+imageName, function(data){
if(data > 0){
console.log("I made it through!");
if(!data.id)
{
$k('#imgUpload').html('');
//$k('#imgPreview').fadeOut();
$k('#newPost').val('');
$k.get("/wall/entry/id/"+data, function(html){
$k('#postList').prepend(html);
});
}
}
});
}
},
send: function(e, data){
var files = data.files;
var duplicates = Array(); // Iterate over all entries and check whether any entry matches the current and add it to duplicates for deletion
for(var i=0; i<data.files.length;i++)
{
for(var j=0;j<data.files.length-1;j++)
{
if(files[i].name == files[j].name && i != j)
{
duplicates.push(j);
}
}
}
if(duplicates.length > 0)
{
for(var i=0;i<duplicates.length;i++)
files.splice(i, 1);
}
console.log("Duplicates");
console.log(duplicates);
},
drop: function(e, data){
console.log("outside");
// $k.each(data.files, function(index, file){
// $k('#imageListDummy').after('<li class="file-small-info-box">'+file.name+'</li>');
// console.log(file);
//
// });
},
add: function(e, data){
upload_data = data;
console.log(data);
$k.each(data.files, function(index, file){
$k('#imageListDummy').after('<li class="file-small-info-box">'+file.name+'</li>');
console.log(file);
});
$j('#post').click(function(event){
upload_data.submit();
if(type == upload_type.image)
{
var file = upload_data.files[0];
console.log("I am here");
console.log(file);
var img = document.createElement("img");
img.src = window.URL.createObjectURL(file);
img.height = 64;
img.width = 64;
img.onload = function(e) {
window.URL.revokeObjectURL(this.src);
}
document.getElementById('imgPreview').appendChild(img);
$k('#imgPreview').show();
}
clickPostCallback(event);
});
$j('#showSubmit').show();
}
});
}
It could be more a browser security issue.
Current file uploads specs don't allow javascript (or anything as far as I know) to tamper with the value of the file field even if to remove it.
So I would imagine any good file uploader would create multiple file upload fields so you can remove the entire field rather than play with the value?
This is speculation though.
Updated answer to Updated Question:
Shouldn't click() only be bound once? you shouldn't need to rebind a click event to a single element '#post' (unless this element changes, in which case it should really be a class). You can place the click() event binding outside of the options for file upload, as long as it's contained in a $(function(){} so it's when the DOM's ready.
Aside from that I'm trying to read the code without any HTML and no experience in multiple file uploading. The best thing to do is try and re-create it on jsfiddle.net, that way others can go in and play around with the code without affecting you and your likely to find the problem while putting the code in there anyway :)

Categories