To handle file uploads on a website we have to use a hidden <input type="file" /> element.
To find out what file has been selected in the Select File Dialog, we can use the onchangeevent.
But how can we detect if the user has hit the cancel button?
Unfortunately, there is no oncancel event.
There exist a lot of workarounds to detect if the user has hit the cancel button, but due to the problem I described here, none of them worked for me reliably.
I've invested countless hours looking for a solution. And now I want to share my solution with you.
I use three event handlers:
onchange event on the file input: To detect when a file has been selected.
onfocus event on window: To detect when the Select File Dialog has been closed.
onmousemove event on document.body: To detect when the interaction is not blocked anymore. Only when this event is called, you can be sure that the onchange event of the input element has been called.
The first two points are obvious and you find them in most proposed solutions. But the crucial point is number 3. In other solutions I sometimes faced the problem that I selected a file, but this selected file has not been propagated to the onchange event handler before window's got focus.
To make a long story short, here's my implementation:
TypeScript solution:
public static selectFile(accept: string = null): Promise<File> {
return new Promise<File>(async resolve => {
const fileInputElement = document.createElement('input') as HTMLInputElement;
fileInputElement.type = 'file';
fileInputElement.style.opacity = '0';
if (accept) fileInputElement.accept = accept;
fileInputElement.addEventListener('change', () => {
const file = fileInputElement.files[0];
console.log('File "' + file.name + '" selected.');
document.body.removeChild(fileInputElement);
resolve(file);
});
document.body.appendChild(fileInputElement);
setTimeout(_ => {
fileInputElement.click();
const onFocus = () => {
window.removeEventListener('focus', onFocus);
document.body.addEventListener('mousemove', onMouseMove);
};
const onMouseMove = () => {
document.body.removeEventListener('mousemove', onMouseMove);
if (!fileInputElement.files.length) {
document.body.removeChild(fileInputElement);
console.log('No file selected.');
resolve(null);
}
}
window.addEventListener('focus', onFocus);
}, 0);
});
}
JavaScript solution:
function selectFile(accept = null) {
return new Promise(async resolve => {
const fileInputElement = document.createElement('input');
fileInputElement.type = 'file';
fileInputElement.style.opacity = '0';
if (accept) fileInputElement.accept = accept;
fileInputElement.addEventListener('change', () => {
const file = fileInputElement.files[0];
console.log('File "' + file.name + '" selected.');
document.body.removeChild(fileInputElement);
resolve(file);
});
document.body.appendChild(fileInputElement);
setTimeout(_ => {
fileInputElement.click();
const onFocus = () => {
window.removeEventListener('focus', onFocus);
document.body.addEventListener('mousemove', onMouseMove);
};
const onMouseMove = () => {
document.body.removeEventListener('mousemove', onMouseMove);
if (!fileInputElement.files.length) {
document.body.removeChild(fileInputElement);
console.log('No file selected.');
resolve(null);
}
}
window.addEventListener('focus', onFocus);
}, 0);
});
}
I'm using cypress since a week, and I succesfully did an integration with the stripe iframe: I've used this code:
in cypress/support/command.js
Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, $iframe => {
const contentWindow = $iframe.prop('contentWindow')
return new Promise(resolve => {
if (contentWindow && contentWindow.document.readyState === 'complete') {
resolve(contentWindow)
} else {
$iframe.on('load', () => {
resolve(contentWindow)
})
}
})
})
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
Cypress.$(selector, document),
)
In cypress/integration/staging-web/web.test.js
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="cardnumber"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('4242424242424242')
}
})
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="exp-date"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('1225')
}
})
cy.get('iframe')
.eq(2)
.iframeLoaded()
.its('document')
.getInDocument('[name="cvc"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('123')
}
})
cy.get('.mt1 > .relative > .input').type('utente')
My problem is that during the page loading, cypress does not wait until stripe fields are fully loaded, and I get an error because happens this (sorry for not-english language, but it's a screenshot):
Those lines are:
cardnumber
expiration date , pin number
4th line is card owner
I've tried with .should('be.visibile') but it does nothing; plus I've tried with
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="cardnumber"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('4242424242424242')
}
})
but no way, it always gives me an error; in this latter case, it doesn't even give an error, it just goes on without filling the fields and after this it stops because it cant go on in the test.
If I add cy.wait(800) before the code in web.test.js it works fine, but I don't want to use wait, because it's basically wrong (what happens if it loads after 5 seconds?).
is there a way to check that those elements must have an height?
Remember that they are in an iframe (sadly).
If I add cy.wait(800) ... it works fine.
This is because you are not using Cypress commands with auto-retry inside getInDocument().
Cypress.$(selector) is jQuery, it just attempts to grab the element, not retry for async loading, and no test failure when not found.
Should use a proper command with retry, like
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
cy.wrap(document).find(selector)
)
or you might need to work from body
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
cy.wrap(document).its('body')
.find(selector)
.should('be.visible')
)
Without a test system I'm not sure exactly which one is correct syntax, but you get the idea.
Also, too many custom commands. You always follow .iframeLoaded() with .its('document'), so just wrap it all up in iframeLoaded custom command.
In fact, resolve(contentWindow.document.body) because it's a better point to chain .find(selector).
This is my test against the Stripe demo page,
Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, $iframe => {
const contentWindow = $iframe.prop('contentWindow')
return new Promise(resolve => {
if (contentWindow && contentWindow.document.readyState === 'complete') {
resolve(contentWindow.document.body)
} else {
$iframe.on('load', () => {
resolve(contentWindow.document.body)
})
}
})
})
it('finds card number', () => {
cy.viewport(1000, 1000)
cy.visit('https://stripe-payments-demo.appspot.com/')
cy.get('iframe')
.should('have.length.gt', 1) // sometimes 2nd iframe is slow to load
.eq(1)
.iframeLoaded()
.find('input[name="cardnumber"]')
.should('be.visible')
})
#Nanker Phelge by the way I updated my function in this way, I posted it here because there was no space in comments:
UPDATE: I put waitForMillisecs inside the function -_- it HAVE TO stay outside! Correction made.
const ADD_MS = 200
const STACK_LIMIT = 15
let stackNumber = 0
let waitForMillisecs = 500
function checkAgainStripe() {
stackNumber++
if (stackNumber >= STACK_LIMIT) {
throw new Error('Error: too many tries on Stripe iframe. Limit reached is', STACK_LIMIT)
}
cy.get('.btn').scrollIntoView().should('be.visible').click()
cy.get('iframe')
.eq(1)
.then($elem => {
//console.log($elem[0].offsetHeight)
//console.log($elem[0])
cy.log('now wait for ' + waitForMillisecs + '; stack number is ' + stackNumber)
cy.wait(waitForMillisecs)
waitForMillisecs = waitForMillisecs + ADD_MS
if (!$elem[0] || !$elem[0].offsetHeight || $elem[0].offsetHeight < 19) {
console.log('entered in if')
cy.reload()
return checkAgainStripe()
}
})
}
I use google MATERIAL COMPONENTS FOR THE WEB and have problems with the "Dialogs".
Check my codepen: Dialog
What do I have to do to have multiple dialogs per page?
JS:
// Find all the dialogs on the page
const dialogEls = Array.from(document.querySelectorAll('.mdc-dialog'));
dialogEls.forEach((ele) => {
const dialog = new mdc.dialog.MDCDialog(ele);
dialog.listen('MDCDialog:accept', function() {
console.log('accepted');
})
dialog.listen('MDCDialog:cancel', function() {
console.log('canceled');
})
// From here I do not know how to continue....
// Here the selector '#dialog-login' should still be dynamic
document.querySelector('#dialog-login').addEventListener('click', function (evt) {
event.preventDefault(evt);
dialog.lastFocusedTarget = evt.target;
// This shows all dialogs, which is wrong.
dialog.show();
})
});
I have updated answer from #Jodo.
I suggest for dynamic approach using data attribute on dialog tags with value of opening button.
// Find all the dialogs on the page
const dialogEls = Array.from(document.querySelectorAll('.mdc-dialog'));
dialogEls.forEach((ele) => {
const dialog = new mdc.dialog.MDCDialog(ele);
dialog.listen('MDCDialog:accept', function() {
console.log('accepted');
})
dialog.listen('MDCDialog:cancel', function() {
console.log('canceled');
})
document.querySelector('#' + ele.dataset.dialog).addEventListener('click', function (evt) {
dialog.show();
});
});
https://codepen.io/woytam/pen/abvdZBQ?editors=1010
Simply add data-dialog attribute to each dialog with value of your button/link. JavaScript function then will use ele.dataset.dialog in foreach.
The Dialog opens twice because you create two event listeners for #dialog-login.
One of them opens the Login Dialog the other one opens the Delivery Dialog.
Since you have two different distinct Dialogs, I would suggest a more static way and declare both dialogs independently:
const dialogLoginEle = document.getElementById('mdc-dialog-login');
const dialogLogin = new mdc.dialog.MDCDialog(dialogLoginEle);
dialogLogin.listen('MDCDialog:accept', function() {
console.log('accepted login');
});
dialogLogin.listen('MDCDialog:cancel', function() {
console.log('canceled login');
});
const dialogDeliveryEle = document.getElementById('mdc-dialog-delivery-condition');
const dialogDelivery = new mdc.dialog.MDCDialog(dialogDeliveryEle);
dialogDelivery.listen('MDCDialog:accept', function() {
console.log('accepted delivery');
});
dialogDelivery.listen('MDCDialog:cancel', function() {
console.log('canceled delivery');
});
document.querySelector('#dialog-login').addEventListener('click', function (evt) {
dialogLogin.show();
});
document.querySelector('#dialog-delivery').addEventListener('click', function (evt) {
dialogDelivery.show();
});
https://codepen.io/j-o-d-o/pen/XZqNYy?editors=1010
Here a "dynamic" approach as requested, but IMHO this is not very readable and error prone.
// Find all the dialogs on the page
const dialogEls = Array.from(document.querySelectorAll('.mdc-dialog'));
// First one is the Login, Second one is the Delivery
var dialogArr = [];
dialogEls.forEach((ele) => {
const dialog = new mdc.dialog.MDCDialog(ele);
dialog.listen('MDCDialog:accept', function() {
console.log('accepted');
})
dialog.listen('MDCDialog:cancel', function() {
console.log('canceled');
})
dialogArr.push(dialog);
});
document.querySelector('#dialog-login').addEventListener('click', function (evt) {
dialogArr[0].show();
});
document.querySelector('#dialog-delivery').addEventListener('click', function (evt) {
dialogArr[1].show();
});
https://codepen.io/j-o-d-o/pen/jZxmxa?editors=1010
How I can programmatically detect when text input filled by typing on keyboard and when it filled automatically by bar-code scanner?
I wrote this answer, because my Barcode Scanner Motorola LS1203 generated keypress event, so I can't use Utkanos's solution.
My solution is:
var BarcodeScanerEvents = function() {
this.initialize.apply(this, arguments);
};
BarcodeScanerEvents.prototype = {
initialize: function() {
$(document).on({
keyup: $.proxy(this._keyup, this)
});
},
_timeoutHandler: 0,
_inputString: '',
_keyup: function (e) {
if (this._timeoutHandler) {
clearTimeout(this._timeoutHandler);
this._inputString += String.fromCharCode(e.which);
}
this._timeoutHandler = setTimeout($.proxy(function () {
if (this._inputString.length <= 3) {
this._inputString = '';
return;
}
$(document).trigger('onbarcodescaned', this._inputString);
this._inputString = '';
}, this), 20);
}
};
Adapted the super useful Vitall answer above to utilize an IIFE instead of prototyping, in case anyone just seeing this now is into that.
This also uses the 'keypress' event instead of keyup, which allowed me to reliably use KeyboardEvent.key, since KeyboardEvent.which is deprecated now. I found this to work for barcode scanning as well as magnetic-strip card swipes.
In my experience, handling card swipes with keyup caused me to do extra work handling 'Shift' keycodes e.g. a Shift code would be followed by the code representing '/', with the intended character being '?'. Using 'keypress' solved this as well.
(function($) {
var _timeoutHandler = 0,
_inputString = '',
_onKeypress = function(e) {
if (_timeoutHandler) {
clearTimeout(_timeoutHandler);
}
_inputString += e.key;
_timeoutHandler = setTimeout(function () {
if (_inputString.length <= 3) {
_inputString = '';
return;
}
$(e.target).trigger('altdeviceinput', _inputString);
_inputString = '';
}, 20);
};
$(document).on({
keypress: _onKeypress
});
})($);
Well a barcode won't fire any key events so you could do something like:
$('#my_field').on({
keypress: function() { typed_into = true; },
change: function() {
if (typed_into) {
alert('type');
typed_into = false; //reset type listener
} else {
alert('not type');
}
}
});
Depending on when you want to evaluate this, you may want to do this check not on change but on submit, or whatever.
you can try following example, using jQuery plugin https://plugins.jquery.com/scannerdetection/
Its highly configurable, time based scanner detector. It can be used as solution for prefix/postfix based, time based barcode scanner.
Tutorial for usage and best practices, as well discussed about various Barcode Scanner Models and how to deal with it. http://a.kabachnik.info/jquery-scannerdetection-tutorial.html
$(window).ready(function(){
//$("#bCode").scannerDetection();
console.log('all is well');
$(window).scannerDetection();
$(window).bind('scannerDetectionComplete',function(e,data){
console.log('complete '+data.string);
$("#bCode").val(data.string);
})
.bind('scannerDetectionError',function(e,data){
console.log('detection error '+data.string);
})
.bind('scannerDetectionReceive',function(e,data){
console.log('Recieve');
console.log(data.evt.which);
})
//$(window).scannerDetection('success');
<input id='bCode'type='text' value='barcode appears here'/>
For ES6 2019 version of Vitall answer.
const events = mitt()
class BarcodeScaner {
initialize = () => {
document.addEventListener('keypress', this.keyup)
if (this.timeoutHandler) {
clearTimeout(this.timeoutHandler)
}
this.timeoutHandler = setTimeout(() => {
this.inputString = ''
}, 10)
}
close = () => {
document.removeEventListener('keypress', this.keyup)
}
timeoutHandler = 0
inputString = ''
keyup = (e) => {
if (this.timeoutHandler) {
clearTimeout(this.timeoutHandler)
this.inputString += String.fromCharCode(e.keyCode)
}
this.timeoutHandler = setTimeout(() => {
if (this.inputString.length <= 3) {
this.inputString = ''
return
}
events.emit('onbarcodescaned', this.inputString)
this.inputString = ''
}, 10)
}
}
Can be used with react hooks like so:
const ScanComponent = (props) => {
const [scanned, setScanned] = useState('')
useEffect(() => {
const barcode = new BarcodeScaner()
barcode.initialize()
return () => {
barcode.close()
}
}, [])
useEffect(() => {
const scanHandler = code => {
console.log(code)
setScanned(code)
}
events.on('onbarcodescaned', scanHandler)
return () => {
events.off('onbarcodescaned', scanHandler)
}
}, [/* here put dependencies for your scanHandler ;) */])
return <div>{scanned}</div>
}
I use mitt from npm for events, but you can use whatever you prefer ;)
Tested on Zebra DS4208
The solution from Vitall only works fine if you already hit at least one key. If you don't the first character will be ignored (if(this._timeoutHandler) returns false and the char will not be appended).
If you want to begin scanning immediately you can use the following code:
var BarcodeScanerEvents = function() {
this.initialize.apply(this, arguments);
};
BarcodeScanerEvents.prototype = {
initialize : function() {
$(document).on({
keyup : $.proxy(this._keyup, this)
});
},
_timeoutHandler : 0,
_inputString : '',
_keyup : function(e) {
if (this._timeoutHandler) {
clearTimeout(this._timeoutHandler);
}
this._inputString += String.fromCharCode(e.which);
this._timeoutHandler = setTimeout($.proxy(function() {
if (this._inputString.length <= 3) {
this._inputString = '';
return;
}
$(document).trigger('onbarcodescaned', this._inputString);
this._inputString = '';
}, this), 20);
}
};
If you can set a prefix to your barcode scanner I suggests this (I changed a bit the Vitall code):
var BarcodeScanner = function(options) {
this.initialize.call(this, options);
};
BarcodeScanner.prototype = {
initialize: function(options) {
$.extend(this._options,options);
if(this._options.debug) console.log("BarcodeScanner: Initializing");
$(this._options.eventObj).on({
keydown: $.proxy(this._keydown, this),
});
},
destroy: function() {
$(this._options.eventObj).off("keyup",null,this._keyup);
$(this._options.eventObj).off("keydown",null,this._keydown);
},
fire: function(str){
if(this._options.debug) console.log("BarcodeScanner: Firing barcode event with string: "+str);
$(this._options.fireObj).trigger('barcode',[str]);
},
isReading: function(){
return this._isReading;
},
checkEvent: function(e){
return this._isReading || (this._options.isShiftPrefix?e.shiftKey:!e.shiftKey) && e.which==this._options.prefixCode;
},
_options: {timeout: 600, prefixCode: 36, suffixCode: 13, minCode: 32, maxCode: 126, isShiftPrefix: false, debug: false, eventObj: document, fireObj: document},
_isReading: false,
_timeoutHandler: false,
_inputString: '',
_keydown: function (e) {
if(this._input.call(this,e))
return false;
},
_input: function (e) {
if(this._isReading){
if(e.which==this._options.suffixCode){
//read end
if(this._options.debug) console.log("BarcodeScanner: Read END");
if (this._timeoutHandler)
clearTimeout(this._timeoutHandler);
this._isReading=false;
this.fire.call(this,this._inputString);
this._inputString='';
}else{
//char reading
if(this._options.debug) console.log("BarcodeScanner: Char reading "+(e.which));
if(e.which>=this._options.minCode && e.which<=this._options.maxCode)
this._inputString += String.fromCharCode(e.which);
}
return true;
}else{
if((this._options.isShiftPrefix?e.shiftKey:!e.shiftKey) && e.which==this._options.prefixCode){
//start reading
if(this._options.debug) console.log("BarcodeScanner: Start reading");
this._isReading=true;
this._timeoutHandler = setTimeout($.proxy(function () {
//read timeout
if(this._options.debug) console.log("BarcodeScanner: Read timeout");
this._inputString='';
this._isReading=false;
this._timeoutHandler=false;
}, this), this._options.timeout);
return true;
}
}
return false;
}
};
If you need you customize timeout, suffix, prefix, min/max ascii code readed:
new BarcodeScanner({timeout: 600, prefixKeyCode: 36, suffixKeyCode: 13, minKeyCode: 32, maxKeyCode: 126});
I also added the isShiftPrefix option to use for example the $ char as prefix with these options: new BarcodeScanner({prefixKeyCode: 52, isShiftPrefix: true});
This is a fiddle: https://jsfiddle.net/xmt76ca5/
You can use a "onkeyup" event on that input box. If the event has triggered then you can called it "Input from Keyboard".
$(window).ready(function(){
//$("#bCode").scannerDetection();
console.log('all is well');
$(window).scannerDetection();
$(window).bind('scannerDetectionComplete',function(e,data){
console.log('complete '+data.string);
$("#bCode").val(data.string);
})
.bind('scannerDetectionError',function(e,data){
console.log('detection error '+data.string);
})
.bind('scannerDetectionReceive',function(e,data){
console.log('Recieve');
console.log(data.evt.which);
})
//$(window).scannerDetection('success');
<input id='bCode'type='text' value='barcode appears here'/>
Hi I have and alternative solution for evaluate a result of the bar code scanner without use of jQuery, first you need and input text that have a focus the moment that the barcode scanner is works
<input id="input_resultado" type="text" />
The code in JavaScript is:
var elmInputScan = document.getElementById('input_resultado');
elmInputScan.addEventListener('keypress', function (e){
clearInterval( timer_response );
timer_response = setTimeout( "onInputChange()", 10);
});
When the barcode scanner input the text call serveral times to the keypress event, but only I interested to the final result, for this reason I use the timer. That's all, you can process the value into the onInputChange function.
function onInputChange() {
console.log( document.getElementById('input_resultado').value );
}
document.addEventListener("keypress", function (e) {
if (e.target.tagName !== "INPUT") {
// it's your scanner
}
});
None of the solutions worked for me because I don't want to focus on an input. I want the result page(item details page) to keep listening for the scanner to scan next item. My scanner fires the keypress event so this worked like a charm for me.
var inputTemp = '';
var inputTempInterval = setInterval(function() {
// change 5 as minimum length of the scan code
if (inputTemp.length >= 5) {
var detected = inputTemp;
inputTemp = '';
clearInterval(inputTempInterval); // stop listening if you don't need anymore
onScannerTrigger(detected);
} else {
inputTemp = '';
}
}, 100);
$(window).keypress(function(e){
inputTemp += String.fromCharCode(e.which);
});
function onScannerTrigger(scannedCode) {
console.log(scannedCode);
// do your stuff
}
I have published a lightweight JS package which doesn't rely on jQuery or input fields. It simple looks at the timing of the keyPress-events to determine wether it was a barcode scanner or regular input.
https://www.npmjs.com/package/#itexperts/barcode-scanner
import {BarcodeScanner} from "#itexperts/barcode-scanner";
let options = {
timeOut: 130,
characterCount: 13
}
let barcodeScanner = new BarcodeScanner(options);
barcodeScanner.addEventListener('scan', function(e){
let barcode = e.detail;
console.log(barcode);
});
I highly recommend this js plugin https://github.com/axenox/onscan.js
It's easy to use and has tones of options to meet your need.
<script src="path-to-onScan.js"></script>
<script>
// Initialize with options
onScan.attachTo(document, {
suffixKeyCodes: [13], // enter-key expected at the end of a scan
reactToPaste: true, // Compatibility to built-in scanners in paste-mode (as opposed to keyboard-mode)
onScan: function(sCode, iQty) { // Alternative to document.addEventListener('scan')
console.log('Scanned: ' + iQty + 'x ' + sCode);
},
onKeyDetect: function(iKeyCode){ // output all potentially relevant key events - great for debugging!
console.log('Pressed: ' + iKeyCode);
}
});
</script>