problem with naturalHeight and naturalWidth Javascript - javascript

I try to sort Images by landscape/portrait format therefor I first select all images on the page with the selectFunction() then I sort them with the function "hochquer" which triggers further actions.
my Problem:
One image shows up with a width and height of 0 in console.log() even though it is properly displayed on my page and has a actual width and height. If I reload the Page for a second time it works perfectly fine and I get a width and height for this Image.
can anyone help me with this?
//Screenshot of the console added
function selectFunction() {
img = document.querySelectorAll(".img");
let imgSelector = Array.from(img);
if (imgSelector % 2 != 0) {
imgSelector.push("even'er");
}
return imgSelector;
}
function hochquer(callback) {
for (let i = 0; i < selectFunction().length; i++) {
console.log(selectFunction()[i]);
let Width = selectFunction()[i].naturalWidth;
console.log(Width, "w");
let Height = selectFunction()[i].naturalHeight;
console.log(Height, "h");
let Ratio = Width / Height;
if (Ratio >= 1) {
//quer
querFormat.push(selectFunction()[i]);
} else {
querFormat.push(undefined);
}
if (Ratio < 1) {
//hoch
hochFormat.push(selectFunction()[i]);
} else {
hochFormat.push(undefined);
}
}
callback();
}
Update:
Screenshot of the console:
[1]: https://i.stack.imgur.com/Jr6P8.png
some more code I use which I think addresses what #54ka mentioned
function newImg() {
let createImg = [];
let imgSrc = [];
const imgContainer = document.querySelector(".gallery");
for (let i = 0; i < Gallery.length; i++) {
createImg.push(new Image());
imgSrc.push(Pfad + Gallery[i]);
createImg[i].setAttribute("src", imgSrc[i]);
createImg[i].setAttribute("class", "Img");
imgContainer.appendChild(createImg[i]);
function checkImage(path) {
return new Promise((resolve) => {
createImg[i].onload = () => resolve({ path, status: "ok" });
createImg[i].onerror = () => resolve({ path, status: "error" });
});
}
loadImg = function (...path) {
return Promise.all(path.map(checkImage));
};
}
loadImg(imgSrc).then(function (result) {
console.log(result);
hochquer(sort);
});
}
newImg();

Edit:
I have made the following changes basing on your comment below after executing previous code.
I'm now adding the event listeners to all images in imgLoadCallback( ) and created a callback function containing code from hochquer( ) function. Please try to run this code.
const imgLoadCallback = function(){
for (let i = 0; i < selectFunction().length; i++) {
console.log(selectFunction()[i]);
let Width = selectFunction()[i].naturalWidth;
console.log(Width, 'w');
let Height = selectFunction()[i].naturalHeight;
console.log(Height, 'h');
let Ratio = Width / Height;
if (Ratio >= 1) {
//quer
querFormat.push(selectFunction()[i]);
} else {
querFormat.push(undefined);
}
if (Ratio < 1) {
//hoch
hochFormat.push(selectFunction()[i]);
} else {
hochFormat.push(undefined);
}
}
}
function selectFunction() {
img = document.querySelectorAll(".img");
img.forEach(i => i.addEventListener('load', imgLoadCallback))
let imgSelector = Array.from(img);
if (imgSelector % 2 != 0) {
imgSelector.push("even'er");
}
return imgSelector;
}
function hochquer(callback) {
imgLoadCallback();
callback();
}
From before edit:
Like #54ka has mentioned in the comments, it is possible that the
image does not load immediately, usually stuff like loading images
from src etc happen in the WEB API Environment asynchronously while
execution , in order to not block the primary execution thread.
Remainder of your code is synchronous, including console.log( )s that
you are performing in order to log the width and height to the
console, so those happen pretty much immediately as soon as that
particular line of code is reached.
Therefore, what I have done in the below code is basically add an
event handler for each of the image tags and only when the image has
completed 'load' the below code inside the handler will be executed
(this is where we are printing the width and height to console now).
Now, this callback function that I have added will only execute after
image loads. So, with this, it will most likely print the correct
width and height to console. But I cannot be sure until we run it at
your end because you are calling a callback at the very end.

It was about a delay in the loading of the picture I fixed it with a new approach by using the onload event and code from the link #54ka provided.
comment of #54ka:
The reason is that when you first load, you take a size from a picture that has not yet been loaded. In the refresh browser, it lays it down and manages to show it before the script passes. To resolve this issue, add an "onload Event" such as: <body onload="myFunction()"> or try this link: Javascript - execute after all images have loaded

Related

Why is this function initiating before it is called? [duplicate]

I need to perform a huge calculation. So, I refer here to create a simple loading screen. Unfortunately, the loading screen is shown after the calculation complete.
Here is my code
class RosterScheduler
{
.........................
autoAssign()
{
var startDate=parseInt($("#autoPlannStartDate").val());
var endDate=parseInt($("#autoPlanEndDate").val());
$("body").addClass("loading");
if (startDate>endDate)
alert("Invalid start date or end date selection");
else
{
if (this.rosterTable.haveInvalidPreferredShift(startDate,endDate))
alert("Invalid shift requirement detected");
else
{
var self=this;
var finalRoster;
var roster,tempAverageSD,lowestAverageSD=100.0;
this.theLowestSDRosters=[];
this.theLowestMissingShiftRosters=[];
for (var i=0;i<100;i++)
{
this._genRoster(startDate,endDate);
}
console.log("Done");
}
}
}
}
I found that if I remark the for loop, the loading screen working properly.
If I uncomment the for loop, the loading screen is shown after the "Done" is shown in the console. How can I fix the problem? I have tried to convert the function _genRoster into a Promise function, however, it does not work.
It looks like _genRoster is blocking the browser, and not giving it any resources to re-render/repaint until the loop is completed. One possibility would be to run the loop after giving the browser a few ms to render the .loading:
this.theLowestSDRosters = [];
this.theLowestMissingShiftRosters = [];
setTimeout(() => {
for (var i = 0; i < 100; i++) {
this._genRoster(startDate, endDate);
}
console.log("Done");
}, 50);
It might be more elegant to call a function with 0 timeout after a single requestAnimationFrame, thus giving the browser time to repaint and then running the heavy loop immediately afterwards:
(warning: the following code will block your browser for a bit)
document.body.style.backgroundColor = 'green';
window.requestAnimationFrame(() => {
console.log('start, setting timeout');
setTimeout(() => {
for (let i = 0; i < 1000000000; i++) {
}
console.log('done');
});
});

Sum the width of all images

I am trying to get the sum of all image widths.
I tried to get the values in an array so that I could calculate the sum of them but still not working, but it should be something really simple indeed, all I want is the sum of the image width values.
componentDidMount() {
let images = document.querySelectorAll('#draggable img');
let widths = [];
images.forEach(each => {
each.addEventListener("load", () => {
widths.push(each.width)
});
});
console.log(widths); // this is logging the array!
const total = widths.reduce(function(a, b){ return a + b; });
console.log("total is : " + total ); // this is crashing!
}
Your widths Array might be empty (your are populating it with a load event) and your are calling reduce on it without initialValue.
This will cause an error, see Array.reduce ,
You could just do this:
widths.reduce((acc, width) => (acc + width), 0);
Update 1, Base on your Codepen and your comments.. The load event listener is not really neaded. There is a compatibily issue with IE < 9, which support attachEvent not addEventListener. I will suggest to use a timer with a recursive function.
sumWidths = () => {
const images = document.querySelectorAll('#draggable img');
let sum = 0;
images.forEach(({ width }) => {
if(!width){ // not width or width 0 means the image has not been fully loaded.
setTimeout(this.sumWidths, 500) // delay half second to allow image to load and try again;
return;
} else {
sum = sum + width;
}
});
// This function can be created on parent component
// or use state management library or what ever befit your needs.
saveImageWidths(sum); // Create this function
// Or save `sum` to the state of this component!!!!
this.setState(state => ({ ...state, widthsSum: sum }));
}
componentDidMount() {
this.sumWidths();
}
Update 2. Using load event listeer
Take a loot at your forked working codepen here
function totalImagesWidth() {
let reportWidth = 0;
let images = document.querySelectorAll('#imagesContainer img');
let imagesWidth = [];
images.forEach(each => {
each.addEventListener("load", () => {
imagesWidth.push(each.width);
if (imagesWidth.length === images.length) {
reportWidth = (imagesWidth.reduce((a, b) => { return a + b; }, 0));
showResult(reportWidth);
}
});
});
function showResult(reportWidth){
const results = document.createElement("p");
results.innerHTML = `
Images: ${images} <br />
Total images: ${images.length} <br />
<code>imagesWidth</code> length: ${imagesWidth.length} <br />
Images widths: ${imagesWidth.toString()} <br />
<b>SUM: ${reportWidth}</b>`;
document.body.appendChild(results);
console.log(imagesWidth);
}
}
totalImagesWidth()
You're trying to get the value of a variable that is supposed to be updated in the future. How about letting it be updated for an expected number of times (which is the total number of images) and then when it has, find the total.
I didn't run my code but am looking at something like
constructor() {
this.widths = [];
}
componentDidMount() {
let images = document.querySelectorAll('#draggable img');
images.forEach(each => {
each.addEventListener('load', () => {
widths.push(each.width);
if (widths.length === images.length) {
this.reportWidth(widths.reduce(function (a, b) { return a + b; }, 0));
}
});
});
}
reportWidth (width) {
console.log(`Width is finally found to be ${width}`);
}
let images = Array.from(document.querySelectorAll('#draggable img'));
reduce is not available on a DOM node collection. Make it an Array first.
Update:
Using your codepen: this logs the desired answer:
const images = Array.from(document.querySelectorAll('#imagesContainer img'));
const sumOfWidths = images.reduce((a, b) => a + b.width, 0);
console.log({sumOfWidths}); // => 600
This assumes the images have already loaded.
Here is a solution for waiting for them to load with promises, which worked in your codepen: (this is not meant to be a good example of using promises - just a simplistic example... consider adding reject handlers for instance)
function totalImagesWidth() {
const images = Array.from(document.querySelectorAll('#imagesContainer img'));
const loadHandler = img => new Promise(resolve => {
img.addEventListener("load", () => resolve({}));
});
Promise.all(images.map(loadHandler)).then(results);
function results() {
const sumOfWidths = images.reduce((a, b) => a + b.width, 0);
const results = document.createElement("p");
results.innerHTML = `
Images: ${images} <br />
Total images: ${images.length} <br />
<b>SUM: ${sumOfWidths}</b>`;
document.body.appendChild(results);
console.log(sumOfWidths);
}
}
totalImagesWidth()
When your reduce happens, you cannot be sure the images are loaded because the event is asynchronous from your function. You need a mechanism to make sure they were all added. Like counting the number of images until the number of items in widths is the same as the number of images.
function whenTotalWidthReady(cb) {
let images = document.querySelectorAll('#draggable img');
if (images.length==0) cb(0);
let widths = [];
images.forEach(each => {
let img = new Image();
img.onload = function() {
widths.push(img.width);
if (widths.length==images.length) {
cb(widths.reduce(function(a, b){ return a + b; }););
}
}
img.src = each.src;
});
}
I don't know if creating intermediary Image objects is mandatory, but that is the way I know works. Also I do not think there is a problem with your reduce. As far as I know, the initial value is optional. But you still can pass "0" as suggested.
Now the way this version works is because it is all asynchronous, you pass a call back function as an argument. And this function will be called with the total width once images are ready.
function componentDidMount() {
whenTotalWidthReady(function(totalWidth) {
console.log("The total width of images is: " + totalWidth);
});
}
Use reduce like so:
const res = [...images].reduce((a, { width = 0 }) => a + width, 0);

Create an loading screen for long calculation

I need to perform a huge calculation. So, I refer here to create a simple loading screen. Unfortunately, the loading screen is shown after the calculation complete.
Here is my code
class RosterScheduler
{
.........................
autoAssign()
{
var startDate=parseInt($("#autoPlannStartDate").val());
var endDate=parseInt($("#autoPlanEndDate").val());
$("body").addClass("loading");
if (startDate>endDate)
alert("Invalid start date or end date selection");
else
{
if (this.rosterTable.haveInvalidPreferredShift(startDate,endDate))
alert("Invalid shift requirement detected");
else
{
var self=this;
var finalRoster;
var roster,tempAverageSD,lowestAverageSD=100.0;
this.theLowestSDRosters=[];
this.theLowestMissingShiftRosters=[];
for (var i=0;i<100;i++)
{
this._genRoster(startDate,endDate);
}
console.log("Done");
}
}
}
}
I found that if I remark the for loop, the loading screen working properly.
If I uncomment the for loop, the loading screen is shown after the "Done" is shown in the console. How can I fix the problem? I have tried to convert the function _genRoster into a Promise function, however, it does not work.
It looks like _genRoster is blocking the browser, and not giving it any resources to re-render/repaint until the loop is completed. One possibility would be to run the loop after giving the browser a few ms to render the .loading:
this.theLowestSDRosters = [];
this.theLowestMissingShiftRosters = [];
setTimeout(() => {
for (var i = 0; i < 100; i++) {
this._genRoster(startDate, endDate);
}
console.log("Done");
}, 50);
It might be more elegant to call a function with 0 timeout after a single requestAnimationFrame, thus giving the browser time to repaint and then running the heavy loop immediately afterwards:
(warning: the following code will block your browser for a bit)
document.body.style.backgroundColor = 'green';
window.requestAnimationFrame(() => {
console.log('start, setting timeout');
setTimeout(() => {
for (let i = 0; i < 1000000000; i++) {
}
console.log('done');
});
});

PDFMake loop using canvas data doesn't keep correct order

I am using the following code to render DOM elements as canvas data and then make a PDF with them.
I have had to create a loop for this because if it was all one image it is impossible to correctly place the data over multiple pages.
$scope.pdfMaker = function() {
var content = [];
var pdfElement = document.getElementsByClassName("pdfElement");
for (var i = pdfElement.length - 1; i >= 0; i--) {
html2canvas(pdfElement[i], {
onrendered: function(canvas) {
var data = canvas.toDataURL();
content.push({
image: data,
width: 500,
margin: [0, 5]
});
console.log(canvas);
}
});
}
var docDefinition = {
content: content
};
setTimeout(function() {
pdfMake.createPdf(docDefinition).download("Score_Details.pdf");
}, 10000);
}
Issues:
I have that nasty 10 second timeout to allow time for processing, how can I restructure my code to allow the PDF to be made after all canvas data has been performed.
My canvas elements are becoming mixed up when they are converted, the correct order is essential. How can I maintain the order of the DOM elements?
It seems that html2canvas returns a promise:
$scope.pdfMaker = function() {
var promises = [];
var pdfElements = document.getElementsByClassName("pdfElement");
for (var i = 0; i < pdfElements.length; i++) {
promises.push(
html2canvas(pdfElements[i]).then(function(canvas) {
console.log('finished one!');
return {
image: canvas.toDataURL(),
width: 500,
margin: [0, 5]
};
})
);
}
console.log('done calling');
$q.all(promises).then(function(content) {
console.log('all done');
pdfMake.createPdf({ content: content }).download("Score_Details.pdf");
console.log('download one');
}, function(err) {
console.error(err);
})
}
To get your code to work just requires a few mods.
The problem from your description is that the rendering time for html2canvas varies so that they come in an undetermined order. If you make the code inside the for loop a function and pass the index to it, the function will close over the argument variable, you can use that index in the onrendered callback because (that will also close over the index) to place the rendered html in the correct position of the content array.
To know when you have completed all the rendering and can convert to pdf, keep a count of the number of html2canvas renderer you have started. When the onrendered callback is called reduce the count by one. When the count is zero you know all the documents have been rendered so you can then call createPdf.
Below are the changes
$scope.pdfMaker = function() {
var i,pdfElement,content,count,docDefinition;
function makeCanvas(index){ // closure ensures that index is unique inside this function.
count += 1; // count the new render request
html2canvas(pdfElement[index], {
onrendered: function(canvas) { // this function closes over index
content[index] = { // add the canvas content to the correct index
image: canvas.toDataURL(),
width: 500,
margin: [0, 5]
};
count -= 1; // reduce count
if(count === 0){ // is count 0, if so then all done
// render the pdf and download
pdfMake.createPdf(docDefinition).download("Score_Details.pdf");
}
}
});
}
// get elements
pdfElement = document.getElementsByClassName("pdfElement");
content = []; // create content array
count = 0; // set render request counter
docDefinition = {content: content}; //
for (i = pdfElement.length - 1; i >= 0; i--) { //do the loop thing
makeCanvas(i); // call the makeCanvas function with the index
}
}
The only thing I was not sure of was the order you wanted the pdfElements to appear. The changes will place the rendered content into the content array in the same order as document.getElementsByClassName("pdfElement"); gets them.
If you want the documents in the revers order call content.reverse() when the count is back to zero and you are ready to create the pdf.
Closure is a very powerful feature of Javascript. If you are unsure of how closure works then a review is well worth the time.
MDN closures is as good a place as any to start and there are plenty of resources on the net to learn from.

Calling function from function WEIRD RESULTS

I was trying to show a text gradually on the screen (like marquee). e.g. H.. He.. Hell.. Hello. when I'm tracing it in debug in VS2010 it's working! but when it's actually running it shows the whole sentence at once.
I made a certain "delay" for about 3 seconds between each letter so it would suppose to take a while, but in reality it's shows everything immediately.
Who's the genius to solve this mystery? (please don't give me advices how to create the marquee effect, it's not the issue anymore. now it's just a WAR between me and javascript!) I'm assuming that it has to do with synchronization when calling function from function?
Thanks to whomever will help me get my sanity back.
you can download the code from here (VS project):
http://pcgroup.co.il/downloads/misc/function_from_function.zip
or view it here:
<body>
<script type="text/javascript">
//trying to display this source sentence letter by letter:
var source = "hi javascript why are you being such a pain";
var target = "";
var pos = 0;
var mayGoOn = false;
//this function calls another function which suppose to "build" the sentence increasing index using the global var pos (it's even working when following it in debug)
function textticker() {
if (pos < source.length) {
flash();
if (mayGoOn == true) {
pos++;
mayGoOn = false;
document.write(target);
textticker();
}
}
}
function flash() {
//I tried to put returns everywhere assuming that this may solve it probably one of them in not necessary but it doesn't solve it
if (mayGoOn == true) { return; }
while (true) {
var d = new Date();
if (d.getSeconds() % 3 == 0) {
//alert('this suppose to happen only in about every 3 seconds');
target = source.substring(0, pos);
mayGoOn = true;
return;
}
}
}
textticker();
</script>
You're obviously doing it wrong. Take a look at this.
var message = "Hello World!";
function print(msg, idx) {
if(!idx) {
idx = 0;
}
$('#hello').html(msg.substring(0, idx));
if(idx < msg.length) {
setTimeout(function() { print(msg, idx + 1) }, 200);
}
}
print(message);
Demo: http://jsbin.com/evehus

Categories