66

I am working with a standard file input for uploads, and I am looking for a way to attach a function to an event when the user clicks/hits enter on the "cancel" button (or escapes out from) the choose file dialog.

I can't find any events that work across all browsers and platforms consistently.

I've read the answers to this question: Capturing cancel event on input type=file but they don't work, as the change event doesn't fire in most browsers on canceling out of the choose file dialog.

I'm looking for a pure js solution, but open to jquery solutions as well.

Anyone solve this problem successfully?

Community
  • 1
  • 1
GWR
  • 1,878
  • 4
  • 26
  • 44
  • Since `.change()` is not executed by all browsers, should look for the event that is called. Are their browsers you are looking to work with specifically? – Twisty Jan 18 '16 at 20:00
  • Some testing with this https://jsfiddle.net/Twisty/j18td9cs/ , in FF, and since 'Cancel' is element of the browsers dialog box, I can only check to see if a file was selected or not. I notice if I select a file, and then browse a 2nd time, and hit cancel, it retains the file value... so that's not helping here. Could create your own Cancel button in the page. Trying to see if something gets updated or return if cancel is selected, like with `confirm()` or `prompt()`. – Twisty Jan 18 '16 at 20:15
  • Does this answer your question? [How to detect when cancel is clicked on file input?](https://stackoverflow.com/questions/4628544/how-to-detect-when-cancel-is-clicked-on-file-input) – leonheess Aug 04 '21 at 21:23

14 Answers14

37

A bit of research indicates that there is no way to detect when Cancel is selected in the File Selection dialog window. You can use onchange or onblur to check if files have been selected or if something has been added to the input value.

This could look like: https://jsfiddle.net/Twisty/j18td9cs/

HTML

<form>
  Select File:
  <input type="file" name="test1" id="testFile" />
  <button type="reset" id="pseudoCancel">
    Cancel
  </button>
</form>

JavaScript

var inputElement = document.getElementById("testFile");
var cancelButton = document.getElementById("pseudoCancel");
var numFiles = 0;

inputElement.onclick = function(event) {
  var target = event.target || event.srcElement;
  console.log(target, "clicked.");
  console.log(event);
  if (target.value.length == 0) {
    console.log("Suspect Cancel was hit, no files selected.");
    cancelButton.onclick();
  } else {
    console.log("File selected: ", target.value);
    numFiles = target.files.length;
  }
}

inputElement.onchange = function(event) {
  var target = event.target || event.srcElement;
  console.log(target, "changed.");
  console.log(event);
  if (target.value.length == 0) {
    console.log("Suspect Cancel was hit, no files selected.");
    if (numFiles == target.files.length) {
      cancelButton.onclick();
    }
  } else {
    console.log("File selected: ", target.value);
    numFiles = target.files.length;
  }
}

inputElement.onblur = function(event) {
  var target = event.target || event.srcElement;
  console.log(target, "changed.");
  console.log(event);
  if (target.value.length == 0) {
    console.log("Suspect Cancel was hit, no files selected.");
    if (numFiles == target.files.length) {
      cancelButton.onclick();
    }
  } else {
    console.log("File selected: ", target.value);
    numFiles = target.files.length;
  }
}


cancelButton.onclick = function(event) {
  console.log("Pseudo Cancel button clicked.");
}

I suggest making your own cancel or reset button that resets the form or clears the value from the input.

Twisty
  • 30,304
  • 2
  • 26
  • 45
  • This doesn't work on Firefox. :( If a file is not previously selected, as soon as I click "Browse" button it logs to console `Suspect Cancel was hit, no files selected.` and `Pseudo Cancel button clicked.`. On clicking cancel in the browse dialog, nothing happens. If a file is previously selected, as soon as I click "Browse" it logs that I selected the previous file, even though I am still in the dialog. – Noitidart Sep 30 '17 at 20:15
  • Wrote this in FF. Wil have to check again. Got fiddle of your test code? – Twisty Sep 30 '17 at 22:23
  • Thanks sir for your fast reply, I just used the fiddle above I didnt write anything seperate. – Noitidart Sep 30 '17 at 23:18
  • @Noitidart it looks like FF fires `focus` followed by `blur` and then `change` if the user selected a file. So the `click` and `blur` events are interrupting the logic here. Updated jQuery: https://jsfiddle.net/Twisty/btf1m9nc/ – Twisty Oct 01 '17 at 17:19
  • The browser might also have problems with not visible input elements. In my case I can hide my onput element with display none or visibility hidden and trigger the dialog with a labeled button. The onfocus event will still be triggered (at least for Chrome on Mac). But testing on smartphones (Chrome on iPhone or HTC U11) revealed that they don't trigger onfocus for this special case. It might not only about the hidden input but that I trigger the dialog via a labeled button instead of the input element itself. – Chaoste Oct 29 '18 at 12:44
16

I have a perfect solution.

The focus event will be executed before the change event.

So I need to use setTimeout to make the focus method execute later than the change method.

const createUpload = () => {
    let lock = false
    return new Promise((resolve, reject) => {
        // create input file
        const el = document.createElement('input')
        el.id = +new Date()
        el.style.display = 'none'
        el.setAttribute('type', 'file')
        document.body.appendChild(el)

        el.addEventListener('change', () => {
            lock = true
            const file = el.files[0]
            
            resolve(file)
            // remove dom
            document.body.removeChild(document.getElementById(el.id))
        }, { once: true })

        // file blur
        window.addEventListener('focus', () => {
            setTimeout(() => {
                if (!lock && document.getElementById(el.id)) {
                    reject(new Error('onblur'))
                    // remove dom
                    document.body.removeChild(document.getElementById(el.id))
                }
            }, 300)
        }, { once: true })

        // open file select box
        el.click()
    })
}



try {
    const file = createUpload()
    console.log(file)
} catch(e) {
    console.log(e.message) // onblur
}
hoythan
  • 466
  • 5
  • 8
3

In your "inputElement.onclick" you should set a flag (in my case I set window.inputFileTrueClosed = true) so you can detect when the window gets the focus after pressing the button "Cancel" for that type of event. The following detect if the window gets the focus again: it means that "Cancel" button could have been pressed:

var isChrome = !!window.chrome;

    window.addEventListener('focus', function (e) {
       
    
    if(window.inputFileTrueClosed != false){
      
        if(isChrome == true){
           setTimeout(
                    function() 
                    {
                         if(window.inputFileTrueClosed != false){
   //if it is Chrome we have to wait because file.change(function(){... comes after "the window gets the focus"
                         window.inputFileTrueClosed = false; 
                         }
                    }, 1000);
}
else
{
        // if it is NOT Chrome (ex.Safari) do something when the "cancel" button is pressed.

        window.inputFileTrueClosed = false;
        
    }
}
})

Obviously the window gets the focus for many other events BUT with the flag you can select the one you need to detect.

giuseppe
  • 105
  • 9
2

When we select the file following events are called -

Scenario 1 : When the select file is clicked and then cancel is clicked

Focus event value=""

Click event value=""

Blur event value=""

Focus event value=""

Blur event value="" (when the user clicks somewhere out)

Scenario 2 : When the file is selected -

Focus event value=""

Click event value=""

Blur event value=""

Focus event value=""

Change event value="filevalue"

Blur event value="filevalue"

Focus event value="filevalue"

Blur event value="filevalue" (when the user clicks somewhere out)

We see here, when the Blur event (last event) is called after focus event with no value of file means that the Cancel button is clicked.

My scenario was to change the Submit button text to "Please wait" while the file is loading currentEvent variable is used to hold the current event value either click or focus if the currentEvent = focus and file value is null means Cancel button is clicked.

Javascript

var currentEvent = "focus";

function onFileBrowse() {    
    var vtbFile = $('#uploadFile')[0].files[0];

    if (vtbFile != undefined) {
        var extension = vtbFile.name.split('.').pop().toLowerCase();
        var valError = "";

        if (extension === 'xlsx' || extension === 'xlsb' || extension === 'csv') {
            if (vtbFile.size === 0)
                valError = "File '" + vtbFile.name + "' is empty";
        }
        else
            valError = "Extension '" + extension + "' is not supported.";

        if (valError !== "") {            
            alert("There was an issue validating the file. " + valError, 20000);
        }        
    }
    //hide Indicator
    var buttonUpload = document.getElementById('btnUploadTB');
    buttonUpload.innerText = "Submit";
};


function fileClick() {
    //show Indicator
    var buttonUpload = document.getElementById('btnUploadTB');
    buttonUpload.innerText = "Please wait..";
    
    document.getElementById('uploadFile').value = null;   
    currentEvent = "click";
}
function fileFocus() {
    currentEvent = "focus";
}

function fileBlur() {
    
    if (!document.getElementById('uploadFile').value && currentEvent == "focus") {
        console.log('blur' + 'change to submit');
        //hide Indicator
        var buttonUpload = document.getElementById('btnUploadTB');
        buttonUpload.innerText = "Submit";
    }
}
HTML

<input class="k-button k-upload-button" type="file" id="uploadFile" name="uploadFile"
    accept=".csv,.xlsx,.xlsb" onChange='onFileBrowse()' onclick="fileClick()" onfocus="fileFocus()" onblur="fileBlur()" />

<button id="btnUploadTB" type="button" class="btn btn-default" onclick="uploadTBFile()" style="width:28%;margin-left: 3px;">Submit</button>
  • Since there is a varying delay between events, this workaround is as good as "notifying" that dialog was change with some delay (in my case 500 sec - which is a lot and makes ui lagg), but in that case, less code is required./ – Antoniossss Aug 27 '20 at 11:38
1

This code does not listen if the cancel button is clicked but it worked for me. It checks if the input has files attached onto it every time its value changes. From there I can pretty much do anything what I need to do.

$("#imageUpload").change(function() {
  if (this.files && this.files[0]) {
    console.log("Has file selected");
  } else {
    console.log("No file selected");
  }
});
<input type="file" id="imageUpload" />

<script src="https://code.jquery.com/jquery-3.5.1.js" integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
jaceB28
  • 11
  • 1
0

I had the problem where I clicked the cancel button on the input type="file" element and wanted the function to do nothing. if something was selected and I clicked the open button then I wanted my function to do something. The example only shows the method, I stripped out what I do with it after it opens. I put in the alerts just so you could see there isn't a filename coming back from the dialog when cancel is clicked. Here is a method I use, it is simple but it works.

 function openFileOnClick(){
    document.getElementById("fileSelector").value = "";
    document.getElementById("fileSelector").files.length = 0;            
    document.getElementById("fileSelector").click();
    if(document.getElementById("fileSelector").files.length >= 1){
        alert(document.getElementById("fileSelector").value);
        //Do something 
    }
    else{
        alert(document.getElementById("fileSelector").value);
        //Cancel button has been called.
    }
}
<html>
<head>
</head>
<body>
<input type="file" id="fileSelector" name="fileSelector" value="" style="display:none;"  />
<input type="button" value="Open File" name="openFile" onclick="openFileOnClick();" />
</body>
</html>
Regular Jo
  • 5,190
  • 3
  • 25
  • 47
DJones
  • 19
  • 1
  • Could you explain in more detail what your code snippet does or why it works? – danjuggler Nov 13 '17 at 21:05
  • I had the problem where I clicked the cancel button on the input type="file" element and wanted the function to do nothing. if something was selected and I clicked the open button then I wanted my function to do something. The above example only shows the method, I stripped out what I do with it after it opens. I put in the alerts just so you could see there isn't a filename coming back from the dialog. – DJones Nov 13 '17 at 21:30
  • 1
    For stuff like this it is helpful to provide a fiddle so we can observe the behavior – GWR Nov 14 '17 at 13:16
  • 1
    @DJones, it'd be great if you could edit your answer and add that information to it! – danjuggler Nov 14 '17 at 16:29
  • 2
    I converted it to a snippet to test it. The alert pops up before the dialog is done. Perhaps you removed something important when you stripped your code down for a demo? – Regular Jo Dec 19 '17 at 20:36
  • 1
    No, this code just runs in IE. In a normal browser, the click triggers asynchronous code. Only in IE the click function is synchronous – Cosmin Popescu Jul 05 '20 at 21:32
0

I was wrestling with a similar issue after implementing an automatic submit of an image upload form using jQuery. If the user cancelled the dialogue it sent a blank. All that was needed was to detect this empty value in the same script:

$('#upload-portrait-input').on('change', function(){
   if ( $(this).val() != '' )
    {
    $('#portraitForm').submit();
    }
   else { // do something when the user clicks cancel
    }
 });
Oroonoko
  • 11
  • 1
0

I needed to style my file upload differently whether the user was browsing the file explorer (active/inactive). Only click and blur events. Just consider that when the user clicks the Cancel button, a click outside the file upload (button in my case) is required.

Here is my solution for Angular, but I guess anyone can grab the idea and adapt it with your favorite framework/library/Vanilla JS.

<!-- HTML - Angular -->

<input hidden type="file" #fileInput (change)="onFileUpload($event.target.files)">
<button (click)="initBrowsingFlags(); fileInput.click()" (blur)="onBlur()" [class.browsing]="browsing">Upload</button>
// Typescript - Angular

/** Whether the user is browsing the file explorer. */
browsing = false;

/** 
 * If a 2nd `blur` event is emitted while {@link browsing} is still true, it means that the user
 * clicked the Cancel button on the file explorer.
 * */
alreadyOneBlurEventWhileBrowsing = false;

onFileUpload(files: FileList) {
  // ...
  this.resetBrowsingFlags();
}

onBlur() {
  if (!this.browsing) return;

  if (this.onCancelClickWhileBrowsing) {
    this.resetBrowsingFlags();
  } else {
    this.onCancelClickWhileBrowsing = true;
  }
}

initBrowsingFlags() {
  this.browsing = true;
  this.alreadyOneBlurEventWhileBrowsing= false;
}

resetBrowsingFlags() {
  this.browsing = false;
  this.alreadyOneBlurEventWhileBrowsing= false;
}
Bruno Martins
  • 882
  • 2
  • 10
  • 21
0

This solves my problem though, although not tested on other browsers except Google Chrome:

$("#fileUpload").on("change", function (event) {
   if (!$(this)[0].files[0]) {
      event.preventDefault();
      return;
   };

   // Else do something here.
});

$(this)[0] is to convert from jQuery object to plain JavaScript object. When you click the Cancel button, the array length for files is zero. You can either use $(this)[0].files.length < 1 or check if $(this)[0].files[0] is undefined.

Antonio Ooi
  • 1,601
  • 1
  • 18
  • 32
  • I love how the edit queue for this answer is full :D I feel you, my eyes hurt too. `$(this)[0]` – Jiří Nov 02 '22 at 15:20
0

I was able to get this to work in Chrome and Safari with inspiration from this answer, however it did not appear to work in Firefox the same way -- the focus event was never triggered when the dialog closed. Looks like Firefox makes use of the cancel event (more info here). I successfully tested this in the latest versions of Chrome, Safari, and Firefox:

function selectFiles(
  options?: {
    /** Allow the selection of multiple files */
    multiple?: boolean,
    /** Restrict the selection to certain types of files (ex: `.txt`) */
    accept?: Array<string>
  }
): Promise<{
  /** The list of selected files (empty if none selected) */
  files: Array<File>,
  /** The event that prompted the dialog to close */
  event: Event
}> {
  return new Promise((resolve) => {
    const fileSelector: HTMLInputElement = document.createElement('input');

    fileSelector.type = 'file';
    fileSelector.style.display = 'none';
    fileSelector.multiple = !!options?.multiple;

    if (Array.isArray(options?.accept)) {
      fileSelector.accept = options.accept.join(',');
    }

    let currTimeout;
    const resolveWithFiles = (event?: Event) => {
      clearTimeout(currTimeout);
      currTimeout = setTimeout(() => {
        // cleanup
        window.removeEventListener('focus', resolveWithFiles);
        fileSelector.remove();
        // resolve with file array and the event associated with
        // what prompted the dialog to close
        resolve({ files: Array.from(fileSelector.files || []), event });
      }, 300);
    };

    // EVENTS
    // "cancel" event in Chrome and Safari
    window.addEventListener('focus', resolveWithFiles);
    // "cancel" event in Firefox
    fileSelector.addEventListener('cancel', resolveWithFiles);
    // files selected
    fileSelector.addEventListener('change', resolveWithFiles);

    // INITIATE
    // open the selection window
    document.body.append(fileSelector);
    fileSelector.click();
  });
}
tdon
  • 1,421
  • 15
  • 24
0
In react , on Change event of input field , the event has no files on cancel event, we can proceed with this assumption. Cancel event will be captured.

  handleChange = (event) => {
    console.log(event);
    console.log(event.target.files[0]);
    this.setState({
      tableDataResult: false,
    });
    if(event.target.files[0]){
      this.setState({
        csvfile: event.target.files[0],
      });
    }else{
//Cancel event called
      console.log("false");
      this.setState({
        csvfile: oldValue,
      });
    }
  };

<input
                                        style={{
                                          width: "450px",
                                          marginLeft: "15px",
                                          marginTop: "5px",
                                        }}
                                        className="csv-input"
                                        type="file"
                                        ref={(input) => {
                                          this.filesInput = input;
                                        }}
                                        name="file"
                                        placeholder={null}
                                        onChange={this.handleChange}
                                      />
Sandeep Jain
  • 1,019
  • 9
  • 13
0

ES2022 Way

Creating a File Picker Service

// wait function to delay 
const wait = (ms) => new Promise((res) => setTimeout(res, ms));

class FilePickerServiceK {
    getFileInput() {
        if (this.ref)
            return this.ref;
        const input = document.createElement('input');
        input.type = 'file';
        this.ref = input;
        return this.ref;
    }
    async pick(opt = {}) {
        const input = this.getFileInput();
        input.multiple = opt.multiple;
        const onWindowFocusP = new Promise((res) => window.addEventListener('focus', res, {once: true}));
        input.click();
        await onWindowFocusP;
        await wait(100);
        const files = Array.from(input.files ?? []);
        input.value = '';
        return files;
    }
}
const FilePickerService = new FilePickerServiceK();
// for demo
const button = document.createElement('button');
button.innerHTML = 'Pick File';
document.body.appendChild(button);
const div = document.createElement('div');
document.body.appendChild(div);
const handle = async () => {
    const [file] = await FilePickerService.pick();
    div.innerHTML = file ? file.name : 'cancelled';
};
button.addEventListener('click', handle);
nkitku
  • 4,779
  • 1
  • 31
  • 27
0

There is an alternative if you can use File System Access API's showOpenFilePicker method.

At the time of writing this answer, the API is fully specs but only Chromium browsers (Chrome and Edge) support this but hopefully it will be more avaiable soon.

(Note that the below snippet doesn't work in sandboxed or cross-origin iframe so StackOverflow, CodePen and jsfiddler cannot run it, you have to have a HTTPS environment to test it)

document.querySelector("button").addEventListener("click", async () => {
    try {
        const file = await globalThis.showOpenFilePicker({
            types: [
                {
                    description: "Images",
                    accept: {
                        "image/*": [".png", ".gif", ".jpeg", ".jpg"],
                    },
                }
            ]
        });

        console.log("You picked: ", file[0].name);
    } catch (e) {
        if (e.code === e.ABORT_ERR) {
            alert("You cancelled.")
        } else {
            throw e;
        }                
    }
});
<button>Pick a file</button>
Luke Vo
  • 17,859
  • 21
  • 105
  • 181
0

There is now a cancel event being fired in such a case, which is supported by all 3 main browser engines since about a year (2 for Chrome and Firefox).

document.querySelector("input").addEventListener("cancel", (evt) => {
  console.log("You closed the file picker dialog without selecting a file.");
});
<input type="file">

Unfortunately, we can't use the oncancel global handler to feature detect this event, since that could point at the homonym event fired on <dialog> elements and thus be a false positive. There is thus no easy detection path to my knowledge.

Kaiido
  • 123,334
  • 13
  • 219
  • 285