Maxium call stack error while cloning HTML element - javascript

I'm making a ticker in JavaScript. There is a very simple HTML markup that whose super parent is fed into a function that creates the ticker. The function basically clones .ticker-inner multiple times (recursively) and append one after the another until its parent's width becomes equal or greater than window width.
However if I move these functions from SU object to window scope, they work fine but right now it throws maximum call stack error.
var SU = {
createTicker: function(tickerWrapper) {
var tickers = tickerWrapper.find('.tickers'),
child = tickers.find('.ticker-inner');
SU.buildTickerChildrenClones(tickers, child);
},
buildTickerChildrenClones: function(tickers, child) {
var tickerWidth = parseInt(tickers.outerWidth(), 10);
var windowWidth = jQuery(window).width();
if (tickerWidth + 35 <= windowWidth) {
child.clone().insertAfter(child);
SU.buildTickerChildrenClones(tickers, child);
}
}
}
I guess the tickers variable loses its reference.

In buildTickerChildrenClones method, nowhere you are setting the outerWidth to a lesser value to create an exit condition
So, in this line
var tickerWidth = parseInt(tickers.outerWidth(), 10);
ticketWidth is always going to give the same value in every buildTickerChildrenClones call.
The function basically clones .ticker-inner multiple times
(recursively) and append one after the another until its parent's
width becomes equal or greater than window width.
So, rather than checking the tickers's outerWidth, you need to check its parent's outerwidth
var tickerWidth = parseInt(tickers.parent().outerWidth(), 10);
This should work, as long as tickers are not designed to wrap themselves to next line.

Related

Best way to present editable tables

I've inherited the job of maintaining and developing an internal journaling system for registering inventory in tables on a local website. It is a website made in PHP, using jquery and handontable to list data from a MySQL database. All fields in the table are editable by the users.
Today the loading of data can be slow (10-15 seconds in the largest tables), which is mainly because of the loops used to populate the table and adjust the column sizes.
What do you think would be the best way to fix this issue? Should I reduce load times by fixing the loops, and keep handsontable as table library? Or should I scrap the old solution and implement something new?
Thanks :)
EDIT
I just saw you're using handsontable so my answer doesn't really provide a solution, as handsontable already uses a kind of list virtualization. I'll leave my answer anyway
Original Answer
What you can probably do is some sort of list virtualization, although this might be a bit tricky with table elements because you need absolute positioning and control of heights. Also it generally assumes that all rows have the same height.
The general idea is you only want to bother with rendering what's currently on the screen. Assuming you can fit 50 rows into your viewport at any time, you're measuring and updating 650 rows that don't matter. If you have 500000 rows, like in the fiddle, you're problem is going to be exponentially out of control.
Without knowing what you're doing exactly, here's a very general approach to the problem:
var elements = [];
var maxLength = 500000; // Number of elements we're going to generate
var itemHeight = 20; // We need a static row height for this to work
var totalHeight = itemHeight * maxLength; // The total height of the content
var $scrollContainer = $('#scroller-container'); // The container that will scroll
var $scrollContent = $('#scroller-contents'); // The content container for our items.
// We need to set the total height of the content so that absolute positioning works and the container receives the correctly sized scroll bar.
$scrollContent.css({ height: `${totalHeight}px` });
// Generate elements.
for (let i = 0; i < maxLength; i++) {
elements.push({
name: `item_${i}`,
value: `value_${i + 100}`
});
}
// By taking some measurements we will find out
// here exactly what items need to be rendered.
function obtainRenderableItems () {
// The size of our scrollable container
var containerHeight = $scrollContainer.height();
// How many items will fit inside the viewable area of our scrollable container
var viewport_count = Math.ceil(containerHeight / itemHeight);
// Where is it currently scrolled to.
var scrollPosition = $scrollContainer.scrollTop();
// The index of the first item in the viewable area
var start = Math.floor(scrollPosition / itemHeight);
// This calculation gives us a number of items to buffer on either side
// which prevents some janky behaviour when scrolling over yet unrendered items
var preScan = start - viewport_count <= 0 ? 0 : start - viewport_count;
// Basically we get the elements visible on the viewports by the current start
// index, and a buffer at the beginning and the end of the same amount of items
// in the viewport.
return elements.slice(preScan, preScan + (viewport_count * 3)).map((element, index) => {
return [preScan + index, element];
});
};
// Convert it to HTML, you can do whatever here, demo only.
function generateHTML (elements) {
return elements.map(el => {
let div = document.createElement('div');
div.className = 'element';
div.style.height = `${itemHeight}px`;
div.style.top = `${el[0] * itemHeight}px`;
div.innerHTML = `${el[1].name} - ${el[1].value}`;
return div.outerHTML;
}).join('');
}
// When we scroll we recalculate what items need to be shown and rerender them
// inside the page.
function onScroll (event) {
let items = obtainRenderableItems();
let htmlContent = generateHTML(items);
$scrollContent.html(htmlContent);
}
$scrollContainer.scroll(onScroll);
// Run at the beginning
onScroll();
The jQuery example above is based on a React component I wrote for exactly this purpose. You'll have to excuse my jQuery I haven't used it in years.
See the fiddle
There are a number of caveats with this approach. The major one being the row height must be the same for all rows, which is not workable for a number of situations. It also relies on a fixed container height, although the flex model can work around this.

JavaScript Div Annimation

I would like to know if there is any specific way to get javaScript code to stop executing at some point so as to allow another called function to execute, for example in a sorting algorithm using Div's you call a function which annimates the swapping of the two Div's so you can visually see the sort taking place. I have tried using the setTimeout(c,t); however it does not seem to be be waiting and the Divs do not seem to be moving, if I however place an alert(""); within the moving code it seems to allow the move to take place with thousands to alerts popping up.
The code I have is as follows:
var q;
var w;
function move(x,y)
{
q = x.style.top; // Keep a reference to the Top divs top element
w = y.style.top; // Keep a reference to the Top divs top element
doMove(x,y);
}
function doMove(topDiv, bottomDiv)
{
//alert("in doMove " + topDiv);
topDiv.style.top = parseInt(topDiv.style.top)+2+'px';
bottomDiv.style.top = parseInt(bottomDiv.style.top)-2+'px';
//alert("hi");
if(bottomDiv.style.top != q && topDiv.style.top != w) // Check if the Top and Bottom divs have finally swapped
{
setTimeout(doMove(topDiv,bottomDiv),20); // call doMove in 20msec
}
}
One problem with your code is that in this line:
setTimeout(doMove(topDiv,bottomDiv),20);
You are calling doMove immediately and passing the value undefined as the first parameter to setTimeout. Not entirely sure that the following will solve your problem, but I would recommend trying this:
setTimeout(function() { doMove(topDiv,bottomDiv); }, 20);

trigger an event when an element reaches a position on the screen

I would like to trigger some functions according to a position of an element. This element's position changes every tenth second. There is two dozens functions to trigger.
I thought about this pseudo-code :
When element position changes{
Loop through all the coordinates to see if a function can be triggered{
if the current element position matches the function's triggering position
execute the function
}
}
But looping through all possible position each split seconds burdens the browser. So if there is a way to have events to do that.
Is it possible ?
Edit:
After Beetroot-Beetroot comment, I must say that the element that moves only moves on an X abscissa : so just one dimension.
It's much like a horizontal timeline moving from left to right, where some animation happen when a certain year is reached.
However the moving speed can be increased by the user, so fixed time to trigger animation is not an option.
There must be many ways to achieve what you want. The code below exploits jQuery's capability to handle custom events to provide a "loosely-coupled" observer pattern.
$(function() {
//Establish the two dozen functions that will be called.
var functionList = [
function() {...},
function() {...},
function() {...},
...
];
var gridParams = {offset:10, pitch:65};//Example grid parameters. Adjust as necessary.
//Establish a custom event and its handler.
var $myElement = $("#myID").data('lastIndex', -1).on('hasMoved', function() {
$element = $(this);
var pos = $element.position();//Position of the moved element relative to its offset parent.
var index = Math.floor((pos.left - gridParams.offset) / gridParams.pitch);//Example algorithm for converting pos.left to grid index.
if(index !== $element.data('lastIndex')) {//Has latest movement align the element with the next grid cell?
functionList[index](index, $element);//Call the selected function.
$element.data('lastIndex', index);//Remember index so it can be tested mext time.
}
});
});
$(function() {
//(Existing) function that moves the element must trigger the custom 'hasMoved' event after the postition has been changed.
function moveElement() {
...
...
...
myElement.trigger('hasMoved');//loosely coupled 'hasMoved' functionality.
}
var movementInterval = setInterval(moveElement, 100);
});
As you can see, an advantage of loose-coupling is that a function and the code that calls it can be in different scopes - .on('hasMoved', function() {...} and myElement.trigger('hasMoved') are in different $(function(){...}) structures.
If you wanted to add other functions to change the position of myElement (eg first, previous, next, last functions), then, after moving the element, they would each simply need to trigger 'hasMoved' to ensure that the appropriate one of your two dozen functions is called, without needing to worry about scopes.
The only thing you need to ensure is that your two dozen functions are scoped such that they can be called by the custom event handler (ie that they are in the same scope or an outer scope, up to and including the global scope).
I've had to make many assumptions, so the code above will not be 100% correct but hopefully it will provide you with a way ahead.

How to select the last element on viewport

I'm looking for an effecient way to constantly select the last element within the visible window/viewport.
So far, this is my code:
$(window).scroll(function () {
$('.post-content p').removeClass("temp last")
$('.post-content p').filter(":onScreen").addClass("temp")
$(".temp").eq(-1).addClass("last")
});
As you could probably imagine, this hauls up a lot of resources and doesn't perform very well. Can somebody please suggest from more elegant code?
My knowledge of Javascript is very basic, so please be patient with me. Thank you.
PS: I am using the onScreen plugin for the :onScreen selector: http://benpickles.github.com/onScreen/
Binding the scroll handler
Binding functions to the scroll Event can lead to serious performance problems. The scroll event fires really vigorously on page scroll, so binding functions with resource-heavy code to it is a bad idea.
What John suggests is setting up the interval and thereby having the code only execute some time after a scroll event.
Have a look at this jsfiddle to see difference between the implementations
The indirect handler solution comes at the cost of a noticeable lag between scrolling and executing the code, and it is your decision if you can trade in performance for snappier execution. Be sure to test performance on every browser you support.
Speeding up code execution
There are a lot of different concepts you can use to speed up your code. Regarding your code, it comes down to:
Caching selectors. You reselect elements every time the scroll handler fires, which is unnecessary
Not using jQuery plugins without knowing what they do. In your case, the plugin code is nice and quite straightforward, but for your goal you can have even snappier code.
preventing any unnecessary calculation. With your and the plugin's code, the offset of every element is calculated every time the scroll handler fires.
So what I've come up with is a Jsfiddle with an example how you could do you scroll handler. It's not exactly matched to your DOM because I don't know your html, but it should be easy to match it to your implementation.
I managed to reduce the time used by 95% compared to your code. You can see for yourself by profiling the two samples in chrome.
I assumed you just want to select the last element and you do not need the temp class
So, here's the code with explanations
// Store the offsets in an array
var offsets = [];
// Cache the elements to select
var elements = $('.elem');
// Cache the window jQuery Object
var jWindow = $(window);
// Cache the calculation of the window height
var jWindowHeight = jWindow.height();
// set up the variable for the current selected offset
var currentOffset;
// set up the variable for the current scrollOffset
var scrollOffset;
// set up the variable for scrolled, set it to true to be able to assign at
// the beginning
var scrolled = true;
// function to assign the different elements offsets,
// they don't change on scroll
var assignOffsets = function() {
elements.each(function() {
offsets.push({
offsetTop: $(this).offset().top,
height: $(this).height(),
element: $(this)
});
});
};
// execute the function once. Exectue it again if you added
// or removed elements
assignOffsets();
// function to assign a class to the last element
var assignLast = function() {
// only execute it if the user scrolled
if (scrolled) {
// assigning false to scrolled to prevent execution until the user
// scrolled again
scrolled = false;
// assign the scrolloffset
scrollOffset = jWindowHeight + jWindow.scrollTop();
// only execute the function if no current offset is set,
// or the user scrolled down or up enough for another element to be
// the last
if (!currentOffset || currentOffset.offsetTop < scrollOffset || currentOffset.offsetTop + currentOffset.height > scrollOffset) {
// Iterate starting from the bottom
// change this to positive iteration if the elements count below
// the fold is higher than above the fold
for (var i = offsets.length - 1; i >= 0; i--) {
// if the element is above the fold, reassign the current
// element
if (offsets[i].offsetTop + offsets[i].height < (scrollOffset)) {
currentOffset && (currentOffset.element.removeClass('last'));
currentOffset = offsets[i];
currentOffset.element.addClass('last');
// no further iteration needed and we can break;
break;
}
}
return true;
} else {
return false;
}
}
}
assignLast();
// reassign the window height on resize;
jWindow.on('resize', function() {
jWindowHeight = jWindow.height();
});
// scroll handler only doing assignment of scrolled variable to true
jWindow.scroll(function() {
scrolled = true;
});
// set the interval for the handler
setInterval(assignLast, 250);
// assigning the classes for the first time
assignLast();

How do I ensure saved click coordinates can be reload to the same place, even if the page layout changed?

I'm storing click coordinates in my db and then reloading them later and showing them on the site where the click happened, how do I make sure it loads in the same place?
Storing the click coordinates is obviously the simple step, but once I have them if the user comes back and their window is smaller or larger the coordinates are wrong. Am I going about this in the wrong way, should I also store an element id/dom reference or something of that nature.
Also, this script will be run over many different websites with more than one layout. Is there a way to do this where the layout is independent of how the coordinates are stored?
Yeah, there are many, many ways a page's layout can alter between loads. Different window sizes, different font sizes, different font availability, different browser/settings (even a small change in layout or font preference can throw out the wrapping). Storing page-relative co-ordinates is unlikely to be that useful unless your page is almost entirely fixed-size images.
You could try looking up the ancestors of the clicked element to find the nearest easily-identifiable one, then make a plot from that element down to the element you want based on which child number it is.
Example using simple XPath syntax:
document.onclick= function(event) {
if (event===undefined) event= window.event; // IE hack
var target= 'target' in event? event.target : event.srcElement; // another IE hack
var root= document.compatMode==='CSS1Compat'? document.documentElement : document.body;
var mxy= [event.clientX+root.scrollLeft, event.clientY+root.scrollTop];
var path= getPathTo(target);
var txy= getPageXY(target);
alert('Clicked element '+path+' offset '+(mxy[0]-txy[0])+', '+(mxy[1]-txy[1]));
}
function getPathTo(element) {
if (element.id!=='')
return 'id("'+element.id+'")';
if (element===document.body)
return element.tagName;
var ix= 0;
var siblings= element.parentNode.childNodes;
for (var i= 0; i<siblings.length; i++) {
var sibling= siblings[i];
if (sibling===element)
return getPathTo(element.parentNode)+'/'+element.tagName+'['+(ix+1)+']';
if (sibling.nodeType===1 && sibling.tagName===element.tagName)
ix++;
}
}
function getPageXY(element) {
var x= 0, y= 0;
while (element) {
x+= element.offsetLeft;
y+= element.offsetTop;
element= element.offsetParent;
}
return [x, y];
}
You can see it in action using this JSFiddle: http://jsfiddle.net/luisperezphd/L8pXL/
I prefer not using the id selector and just going recursive.
function getPathTo(element) {
if (element.tagName == 'HTML')
return '/HTML[1]';
if (element===document.body)
return '/HTML[1]/BODY[1]';
var ix= 0;
var siblings= element.parentNode.childNodes;
for (var i= 0; i<siblings.length; i++) {
var sibling= siblings[i];
if (sibling===element)
return getPathTo(element.parentNode)+'/'+element.tagName+'['+(ix+1)+']';
if (sibling.nodeType===1 && sibling.tagName===element.tagName)
ix++;
}
}
Your coordinates should be relative to the page contents, not the window. Use the upper-left of your HTML as the origin.
You will need to do a calculation to determine this at the time the data is recorded.
It probably depands on the meaning of the click. That is, are you concerned about which elements of the page that you user clicked on? If that is the case then I would store the coordinates relative to the element.
So the user clicked on element X. Do you need the precise location in element X? Then store that coordinate with the origin at top left of the element itself. This way, when the element moves relative to other content on the page then the position within the element remains valid.
I was hoping that someone had a much more brilliant solution to this problem, but my original thoughts must be the only way to effectively do this.
Each website must have a base setup described (e.g. [Centered layout, 960px] or [Fluid layout, Col1: 25%, Col2: 60%, Col3: 15%]
Click coordiantes must be recorded in relation to the screen:x/scroll:y along with screen coordinates.
On return the click coords will look at the stored layout, current screen size and calculate based on that.
I'm doing something similar here where I need to record where an element was drag and dropped on the page. I can store some data of the drop location in a database, in order to pull it out and place the element back where it was dropped. The only requirement is that I want the dropped element to be as close as possible to the element on which it was dropped, on all screen sizes.
Due to the responsive nature of the modern web, elements can move to completely different locations depending on screen size.
My solution is to ignore all DOM selectors, and instead simply record where the element is in the DOM tree by recording a child index on every 'layer' of the DOM, all the way down to to the element in question.
I do this by traversing up the DOM tree from the event.target element, looking at currentNode.parentNode.children to find which child index my currentNode inhabits. I record that in an array, which I can then use to index all the way back down the DOM tree to find my element. I also save the dropped elements offset as a percentage, in case the dropzone element has changed pixel size.
Here's my cut down code:
var rect = mouseEvent.target.getBoundingClientRect()
// get position of mouseEvent in target as a percentage so we can be ok if it changes size
var xpos = Math.floor(mouseEvent.offsetX / rect.width * 100)
var ypos = Math.floor(mouseEvent.offsetY / rect.height * 100)
// traverse backwards up the dom tree, recording this 'branch' of it as we go:
var curEl = mouseEvent.target
var tree = []
while(curEl.parentNode){
for( var i = 0; i < curEl.parentNode.children.length; i ++ ){
var curChild = curEl.parentNode.children[i]
if( curChild === curEl ){ // i is our child index
tree.unshift(i) // unshift to push to the front of the array
break
}
}
curEl = curEl.parentNode
}
And then in order to find my node again, I simply traverse back down the dom:
var curEl = document
for(var i = 0; i < tree.length; i ++){
curEl = curEl.children[tree[i]]
}
All I save to the database is the tree array (a flat array of integers - how can you get smaller?) and my x and y offsets!

Categories