How to remove duplicate JavaScript code of DOM event handler? - javascript

I'm trying to remove duplicate JavaScript code. I have a page with many <input type="file">. Each loads an image and performs some distinct processing. The problem is that I have many duplicates of the following code:
inputFile1.onchange = function (e) {
var file = e.target.files[0];
if (typeof file == 'undefined' || file == null) {
return;
}
var imageType = /image.*/;
if (!file.type.match(imageType)) {
window.alert('Bad file type!');
return;
}
var reader = new FileReader();
reader.onloadend = function (e) {
var imageLoader = new Image();
imageLoader.onload = function () {
// process image
};
imageLoader.src = e.target.result;
};
reader.readAsDataURL(file);
};
inputFile2.onchange = ... (repeats all but process image)
inputFile3.onchange = ... (repeats all but process image)
Only the code at process image comment varies. How can I remove the surrounding duplicate code?
I know that JavaScript functions are objects. How can I define a function object and create one distinct instance for each event handler, passing a different function for process image to each object?

You can make a generator for such functions with a closure taking the individual callback as an argument:
function getChangeHandler(loadCallback) {
return function (e) {
var file = e.target.files[0];
if (typeof file == 'undefined' || file == null) {
return;
}
var imageType = /image.*/;
if (!file.type.match(imageType)) {
window.alert('Bad file type!');
return;
}
var reader = new FileReader();
reader.onloadend = function (e) {
var imageLoader = new Image();
imageLoader.onload = loadCallback; // <= uses the closure argument
imageLoader.src = e.target.result;
};
reader.readAsDataURL(file);
};
}
inputFile1.onchange = getChangeHandler(function() { /* custom process image */ });
inputFile2.onchange = getChangeHandler(function() { /* custom process image */ });
inputFile3.onchange = getChangeHandler(function() { /* custom process image */ });
An other, eventually superior approach would be to use only one change-event handler for all inputs, that dynamically chooses the custom image processor by the name or id of the input:
var imageProcessors = {
"box1": function() { … },
"anotherbox": function() { … },
…
};
function changeHandler(e) {
var input = this; // === e.target
…
reader.onloadend = function (e) {
…
imageLoader.onload = imageProcessors[input.id];
};
}
// and bind this one function on all inputs (jQuery-style):
$("#box1, #anotherbox, …").click(changeHandler);

You can write a function that returns a function:
function processFile(callback) { //callback is the unique file processing routine
return function(e) {
var file = e.target.files[0];
if (typeof file == 'undefined' || file == null) {
return;
}
var imageType = /image.*/;
if (!file.type.match(imageType)) {
window.alert('Bad file type!');
return;
}
var reader = new FileReader();
reader.onloadend = function (e) {
var imageLoader = new Image();
imageLoader.onload = callback; //Put it here!
imageLoader.src = e.target.result;
};
reader.readAsDataURL(file);
};
}
Then call like this:
inputFile1.onchange = processFile(function() {
//file processing for number 1
});
inputFile2.onchange = processFile(function() {
//file processing for number 2
});
inputFile3.onchange = processFile(function() {
//file processing for number 3
});

Here's an EMCA5 solution, just to throw it into the mix. It binds a dynamic event callback depending on the element.
I've assumed each field has an ID (input1 etc) but with some modification of the code (i.e. identifying the trigger element by some other means) this wouldn't be necessary.
Array.prototype.slice.call(document.querySelectorAll('input[type=file]')).forEach(function(element) {
/* prepare code specific to the element */
var input_specific_code = (function() {
switch (element.id) {
case 'input1': return function() { /* #input1 code here */ };
case 'input2': return function() { /* #input2 code here */ };
case 'input3': return function() { /* #input3 code here */ };
}
})();
element.addEventListener('change', (function(input_specific_code) { return function(evt) {
var id_of_trigger_input = element.id;
/* common code here */
/* element-specific code */
input_specific_code();
/* continuation of common code */
}; })(input_specific_code), false);
});

Related

How to convert a image src from a blob string to data URI

I have a page where the user can paste an image into a content editable div. When I get the image the src returns a string. When I look in debug tools this is what I see:
<img src="blob:http://www.example.com/3955202440-AeFf-4a9e-b82c-cae3822d96d4"/>
How do I convert that to a base 64 string?
Here is the test script, http://jsfiddle.net/bt7BU/824/.
// We start by checking if the browser supports the
// Clipboard object. If not, we need to create a
// contenteditable element that catches all pasted data
if (!window.Clipboard) {
var pasteCatcher = document.createElement("div");
// Firefox allows images to be pasted into contenteditable elements
pasteCatcher.setAttribute("contenteditable", "");
// We can hide the element and append it to the body,
pasteCatcher.style.opacity = 0.5;
document.body.appendChild(pasteCatcher);
// as long as we make sure it is always in focus
pasteCatcher.focus();
document.addEventListener("click", function() { pasteCatcher.focus(); });
}
// Add the paste event listener
window.addEventListener("paste", pasteHandler);
/* Handle paste events */
function pasteHandler(e) {
// We need to check if event.clipboardData is supported (Chrome)
if (e.clipboardData) {
// Get the items from the clipboard
var items = e.clipboardData.items || e.clipboardData.files;
var itemcount = items ? items.length : 0;
pasteArea.value = "items found:"+itemcount;
if (itemcount) {
// Loop through all items, looking for any kind of image
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
// We need to represent the image as a file,
var blob = items[i].getAsFile();
// and use a URL or webkitURL (whichever is available to the browser)
// to create a temporary URL to the object
var URLObj = window.URL || window.webkitURL;
var source = URLObj.createObjectURL(blob);
// The URL can then be used as the source of an image
createImage(source);
}
}
} else {
console.log("no items found. checking input");
// This is a cheap trick to make sure we read the data
// AFTER it has been inserted.
setTimeout(checkInput, 1);
}
// If we can't handle clipboard data directly (Firefox),
// we need to read what was pasted from the contenteditable element
} else {
console.log("checking input");
// This is a cheap trick to make sure we read the data
// AFTER it has been inserted.
setTimeout(checkInput, 1);
}
}
/* Parse the input in the paste catcher element */
function checkInput() {
console.log("check input");
// Store the pasted content in a variable
var child = pasteCatcher.childNodes[0];
// Clear the inner html to make sure we're always
// getting the latest inserted content
//pasteCatcher.innerHTML = "";
//console.log( "clearing catcher");
console.log(child);
if (child) {
// If the user pastes an image, the src attribute
// will represent the image as a base64 encoded string.
if (child.tagName === "IMG") {
createImage(child.src);
reader = new FileReader();
reader.readAsDataURL(child.src);
reader.loadend = function(e) {
console.log(e.target.result);
}
}
}
}
/* Creates a new image from a given source */
function createImage(source) {
var pastedImage = new Image();
pastedImage.onload = function(e) {
//pasteArea.text = pastedImage.src;
console.log(1);
console.log(e);
loadImage.src = e.target.src;
console.log(loadImage.src);
}
pastedImage.src = source;
}
<textarea id="pasteArea" placeholder="Paste Image Here"></textarea>
<img id="loadImage" />
I'm testing this in Safari on Mac.
Since the blobURI is generated automatically by the browser, you can use this, which will download the produced image as a new Blob:
const toDataURL = url => fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(blob)
}))
And then on your function createImage(source) { you can call it:
toDataURL(source)
.then(dataUrl => {
console.log('RESULT:', dataUrl)
})
This answer is complimentary to #BrunoLM's answer for when you don't have ES6 or you want to read in a different image type.
ES6:
const toDataURL = url => fetch(url)
.then(response => response.blob())
.then(blob => new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(blob)
}))
Not ES6 (seems to work the same):
const toDataURL = function(url) {
return fetch(url).then(function(response) {
return response.blob();
}).then(function (blob) {
var type = blob.type;
var size = blob.size;
return new Promise(function(resolve, reject) {
const reader = new FileReader();
reader.onerror = reject;
reader.readAsDataURL(blob);
reader.onloadend = function() {
return resolve(reader.result);
}
}
)}
)}
Based on my understanding of ES6 (ES6 to not ES6):
var a = url => fetch(url)
var a = function(url) { return fetch(url) }
var a = function(parameter) { return statement }
var b = (parameter, parameter) => { fetch(param, param) }
var b = function(foo1, foo2) => { return fetch(param, param) }
var c = url = () => resolve(reader.result)
var c = url = function() { return resolve() }
Making a call:
toDataURL(url).then(function(dataUrl) {
console.log("RESULT:" + dataUrl);
});
Note:
The value returned by the above method is of type "image/tiff" when run in Safari on OSX. If you want to specify another type, such as PNG, there more info on that here.

Return true/false from asynchronous function to use in synchronous if()

I'm creating simple upload form (for .zip files) and I want to validate if zip contains all files which will be necessary later.
so I have a function isZipCorrect():
isZipCorrect = function() {
'use strict';
if (this.name.slice(-3) === 'zip') {
var fileReader = new FileReader();
var zip = new JSZip();
var shpCorrect = false;
fileReader.onload = function() {
var zip = new JSZip(this.result);
shpCorrect = zip.file(/.*?/).every(function(file) {
return (file.name.slice(-3) === 'shp' ||
file.name.slice(-3) === 'dbf' ||
file.name.slice(-3) === 'shx');
});
console.log(shpCorrect);
};
fileReader.readAsArrayBuffer(this.file);
return shpCorrect;
} else {
return true;
}
and I use it in if(isZipCorrect()) before XMLHttpRequest.
I assume that the problem is asynchronous function (fileReader.onload) which end operation when whole code is already ended. But I don't want to call send function from fileReader.onload because for me checking is Zip correct must be optional (you should be able to upload other files which will go through without 'parsing')
You could use jQuery.Deferred() to do something like this:
validateZipFile = function(file) {
var
deferred = $.Deferred(),
fileReader = new FileReader();
fileReader.onload = function() {
var zip = new JSZip(this.result);
var isCorrect = zip.file(/.*?/).every(function(file) {
return /\.(shp|dbf|shx)$/i.test(file.name)
});
isCorrect ? deferred.resolve() : deferred.reject();
};
if( !/\.(zip)$/i.test(file.name) ) {
deferred.reject()
} else {
fileReader.readAsArrayBuffer(file)
}
return deferred.promise()
};
and call it like so:
validateZipFile(file).done(submitForm).fail(handleBadZip);
where 'submitForm' and 'handleBadZip' are functions you've previously defined
you can pass a callback function as a parameter to isZipCorrect and in callback function you can use send function:
isZipCorrect = function(callback) {
'use strict';
if (this.name.slice(-3) === 'zip') {
// all the existing code
fileReader.onload = function() {
var zip = new JSZip(this.result);
shpCorrect = zip.file(/.*?/).every(function(file) {
callback((file.name.slice(-3) === 'shp' ||
file.name.slice(-3) === 'dbf' ||
file.name.slice(-3) === 'shx'));
});
};
fileReader.readAsArrayBuffer(this.file);
//return shpCorrect;
} else {
callback(true);
}

read multiple file inputs

This is the code I have for reading the first item in a file input, how can I iterate over all items inside this input?
function readFile (uploadControlId) {
if (!window.FileReader)
throw "The browser does not support HTML 5";
var element = document.getElementById(uploadControlId);
var def = new $.Deferred();
var file = element.files[0];
var parts = element.value.split("\\");
var fileName = parts[parts.length - 1];
var reader = new FileReader();
reader.onload = function (e) {
if (uploadControlId == 'uploadControlId'){
def.resolve(e.target.result, fileName);
} else {
def.resolve(e.target.result, fileName);
}
};
reader.onerror = function (e) {
def.reject(e.target.error);
};
reader.readAsArrayBuffer(file);
return def.promise();
}
I have tried something like:
angular.forEach(element.files, function (file){
})
But this doesn't work since the variables 'parts' and 'fileName' is from the variable 'element', so if I iterate over each file in element, they get 'undefined' fileName, this means they won't have like .txt or .pdf, so they are unreadable.
Update: This give no error, but only the last file gets uploaded:
function readFile (uploadControlId) {
if (!window.FileReader)
throw "The browser does not support HTML 5";
var def = new $.Deferred();
var element = document.getElementById(uploadControlId);
angular.forEach(element.files, function(file){
var fileName = file.name;
var reader = new FileReader();
reader.onload = function (e) {
def.resolve(e.target.result, fileName);
};
reader.onerror = function (e) {
def.reject(e.target.error);
};
reader.readAsArrayBuffer(file);
});
return def.promise();
}
My upload function:
$scope.UploadAttachment = function(){
readFile(uploadControlId).done(function (buffer, fileName) {
// logic to upload to server
}).fail(function (err) {
alert("error in reading file content");
});
};
Have you added the "multiple" attribute on the input tag?
By the way if you add this directive to your tag, an event will be fired with all files and you will handle that in you controller.
// Directive
(function(){
var Directive = function(){
return {
restrict: 'A',
scope : {},
link : function(scope, element, attrs){
element.bind('change', function(changeEvent){
scope.$emit('fileReader', changeEvent.target.files);
});
}
}
};
Directive.$inject = [];
app.directive('fileReader', Directive);
})();
// Controller
(function(){
var Controller = function($scope){
// Methods
function fileReader(files){
for(var iFile = 0, fileLen = files.length; iFile < fileLen; iFile = iFile + 1){
var file = files[iFile];
// Do something
}
}
// Events
$scope.$on('fileReader', function(event, files){
fileReader(files);
});
};
Controller.$inject = [
'$scope'
];
app.controller('MainCtrl', Controller);
})();

JavaScript Async readAsDataURL multiple files

I have a list of files I need to save and in addition to the name I need to send the readAsDataURL to the server as well.
The problem is I am not sure how to do it with the async nature of readAsDataURL. Because to save the DATAURL to the array I need to look up the name of the file which is in the files list. and I cannot pass the file to the async method of readAsDataURL. How do you write this properly to work? The end result is I want a list of files sent to the server in one JSZip file.
function saveFileList(files)
{
for (var i = 0, file; file = files[i]; i++) {
var fr = new FileReader();
fr.onload = function(e){
if (e.target.readyState == FileReader.DONE) {
var tt = e.target.result.split(",")[1];
//update the record in the list with the result
}
};
var pp = fr.readAsDataURL(file);
}
If you have FileList and you need to get array of base64 string, you need do this
export async function fileListToBase64(fileList) {
// create function which return resolved promise
// with data:base64 string
function getBase64(file) {
const reader = new FileReader()
return new Promise(resolve => {
reader.onload = ev => {
resolve(ev.target.result)
}
reader.readAsDataURL(file)
})
}
// here will be array of promisified functions
const promises = []
// loop through fileList with for loop
for (let i = 0; i < fileList.length; i++) {
promises.push(getBase64(fileList[i]))
}
// array with base64 strings
return await Promise.all(promises)
}
use it like this
const arrayOfBase64 = await fileListToBase64(yourFileList)
You need another function around it, so you can pass the file in. Try this:
var reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
if(reader.readyState == FileReader.DONE)
alert(theFile.name); // The file that was passed in.
}
};
})(file);
reader.readAsDataURL(file);
An alternative to Russell G's answer:
var reader = new FileReader();
reader.onload = function(event){
payload = event.target.result;
var filename = file.name, filetype = file.type;//etc
//trigger a custom event or execute a callback here to send your data to server.
};
reader.onerror = function(event){
//handle any error in here.
};
reader.readAsDataURL(file);

Waiting for a file to load (onload JavaScript)

I want to read a text from a file and return it in a function. So here's the important part of my code:
function getFileRequest(id, contentType, callback) {
var val = "x";
if (window.File && window.FileReader && window.FileList && window.Blob) {
var element = document.getElementById(id);
var file = element.files[0];
if(file != null) {
if(file.type.match("text/xml")){
var r;
r = new FileReader();
r.onload = function (e) {
val = e.target.result;
}
r.readAsText(file);
}
else
alert("Wrong file format");
}
} else {
alert('The File APIs are not fully supported by your browser.');
}
alert(val);
if(val == null)
return "";
else
return getRequestBody(id,contentType,val);
}
I want to pass the text to a variable called "val". But, as it seems to me at least, alert(val) is always showing default "x" because probably it's not waiting for onload function to be executed. Am I right at all?
How can I get an access to that text then? Is there a way to wait for an excecution?
Of course the alert isn't in the onload function, so it's called immediately.
You may do that :
var val = "x";
//... code to load a file variable
var r;
r = new FileReader();
r.onload = function (e) {
val = e.target.result;
r.readAsText(file);
alert(val);
};
You cannot wait and stop the execution of your code, so the general idea is to defer it using a callback.
Supposing the code you show is really to be done in two parts, one doing file manipulation and the other one using it, it could be like this :
function fetchVal(callback) {
var val = "x";
//... code to load a file variable
var r;
r = new FileReader();
r.onload = function (e) {
val = e.target.result;
r.readAsText(file);
callback(val);
};
}
fetchVal(function(val){
alert(val);
// use val
});

Categories

Resources