Best way to present editable tables - javascript

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.

Related

Swap Items using basiljs

I am pretty new to the whole basiljs world. So this might be a very basic question. I wasn't able to figure it out on my own however… 
I am trying to create a simple script that swaps two Items that are selected on the same page.
I was able to get the image frame to swap, however it leaves the frames content in the same position. Here is waht it looks like:
#includepath "~/Documents/;%USERPROFILE%Documents";
#include "basiljs/bundle/basil.js";
function draw() {
var selItems = b.selections();
var selItems0x = b.itemX(selItems[0]);
var selItems1x = b.itemX(selItems[1]);
var selItems0y = b.itemY(selItems[0]);
var selItems1y = b.itemY(selItems[1]);
b.itemX(selItems[0], selItems1x);
b.itemX(selItems[1], selItems0x);
b.itemY(selItems[0], selItems1y);
b.itemY(selItems[1], selItems0y);
}
b.go();
Now my question is: How can I call on the frames content. Obviously I want that one the move the same with the frame.
Thanks for your help, I am eager to learn more!
Raphy
Even though it is not the "basiliy" way I suggest using InDesign build in functions. You can mix them with Basil Code. Basil doesn't care. There is the possibility to fit elements into its frame or center them.
Try this snippet:
#includepath "~/Documents/;%USERPROFILE%Documents";
#include "basiljs/bundle/basil.js";
function setup(){
var sel = b.selections();
var gb0 = sel[0].geometricBounds;
var gb1 = sel[1].geometricBounds;
// swap them
sel[0].geometricBounds = gb1;
sel[1].geometricBounds = gb0;
// see the different fit options
// http://yearbook.github.io/esdocs/#/InDesign/FitOptions
sel[0].fit(FitOptions.CENTER_CONTENT);
sel[0].fit(FitOptions.PROPORTIONALLY);
}
b.go();
The problem is that in InDesign a (image) graphic and its containing frame are treated as two separate objects, which means, if you only move the frame, the graphic in it does not move along.
In basil.js you have the method b.transformImage() to move a frame along with its graphic, but it is rather cumbersome to use, as you need to pass along the position as well as the scale of the image.
Alternatively you could move the graphic in a second step. Make sure first that the item actually contains a graphic (instead of being a simple oval etc.) and if that's the case move it to the same position as its parent frame. You can access a frames graphic by using frame.graphics[0].
The script would look like this then:
#includepath "~/Documents/;%USERPROFILE%Documents";
#include "basiljs/bundle/basil.js";
function draw() {
var selItems = b.selections();
var selItems0x = b.itemX(selItems[0]);
var selItems1x = b.itemX(selItems[1]);
var selItems0y = b.itemY(selItems[0]);
var selItems1y = b.itemY(selItems[1]);
b.itemX(selItems[0], selItems1x);
b.itemY(selItems[0], selItems1y);
if(selItems[0].graphics.length > 0) {
b.itemX(selItems[0].graphics[0], selItems1x);
b.itemY(selItems[0].graphics[0], selItems1y);
}
b.itemX(selItems[1], selItems0x);
b.itemY(selItems[1], selItems0y);
if(selItems[1].graphics.length > 0) {
b.itemX(selItems[1].graphics[0], selItems0x);
b.itemY(selItems[1].graphics[0], selItems0y);
}
}
b.go();
Note that this will not work if the top left corner of the image is cropped by the frame. In this case you would need to figure out where the top left corner of the graphic actually is and then offset the graphic accordingly after moving it.
Btw., the basil.js team is aware that the transforming of images is somewhat overly complicated and there is a plan to simplify this process in the future.

How to make a game out of an html table?

I'm basically trying to make a game that involves a grid. Here's what I have so far (it'll help to see the game before I explain what I need to happen):
Javascript (see jsfiddle for html):
var score = 0;
var points = function(val, box) {
var noise = Math.round(Math.round(0.1*val*Math.random()*2) - 0.1*val);
score = score + (val + noise);
var square = document.getElementById(box);
square.innerHTML = val + noise;
square.style.display='block';
setTimeout(function() {
square.style.display='none';
}, 400);
document.getElementById("out").innerHTML = score;
}
http://jsfiddle.net/stefvhuynh/aTQW5/1/
The four red squares at the bottom left of the grid needs to be the starting point in the game. When you click on one of those boxes, you can then travel along the grid by clicking adjacent boxes. Basically, I need to make it so that the player can only travel up, down, left, and right from the box that they just clicked on. I don't want the points function to be invoked when the player clicks on a box that they're not supposed to click on.
Additionally, I need to make it so that the player can't click on another box until 400 ms have elapsed.
I'm relatively new to programming so any help at all would be great. I would also appreciate tips on how to make the program more efficient, if there's a way to do that.
General idea:
I'd suggest having a similar id for all your boxes, such as box_x_y, and storing a list of strings, let's say allowedSquares.
You would then be able to write a function which, upon clicking on a box, would check if it's id is in allowedSquares, and if it is, call points(val, box) then update the contents of allowedSquares to reflect the change of position.
The point of using a standard id convention for all your boxes is that you could write getPosition(box) and getBox(intX, intY) that would parse the id strings to return you the box position, or vice-versa.
You can even make the updateAllowedSquares(clickedBox) function change the color of adjacent boxes to show they're allowed next steps.
EDIT: Some example code:
Disclaimer: these are not the code lines you're looking for.
This is only a starting kit for you, which assumes a 3x3 grid with a single-square bottom right starting position. You will have to adapt this code a bit. Also, I predict something will go wrong concerning going out of bounds. I'll let you think with this a bit, as I prefer giving food for thoughts over complete solutions in those cases...
var allowedSquares = ["box_2_2"]; // Initial list
function decodePositionFromID(boxId) {
return boxId.split("_").slice(1,2);
}
function getIDfromXY(x, y) {
return "box_" + x + "_" + y;
}
function updateAllowedSquaresList(boxID) {
// 1 - We clear the array.
allowedSquares.length = 0;
// 2 - We get the adjacent boxes IDs.
var xyArray = decodePositionFromID(boxId);
var upperBoxID = getIDfromXY(xyArray[0], xyArray[1]-1);
// Rince, repeat, and add some gameboard boundaries checks.
// 3 - We add the new IDs to the list.
allowedSquares.push(upperBoxID, ...);
}
function boxClick(val, boxID) {
// We check if the box is a valid square to play.
if (allowedSquares.indexOf(boxID) != -1) {
points(val, boxID);
updateAllowedSquaresList(boxID);
}
}

How to increase a CSS value by the value of a JS variable?

I have several fixed position divs with the same class at varying distances from the left edge of the window, and I'd like to increase/decrease that distance by an equal amount on each div when a certain action happens (in this case, the window being resized). I've tried positioning them with CSS and percentages rather than pixels, but it doesn't quite do the job.
Is there a way to store the position of each of those divs in an array and then add/subtract a given amount of pixels?
Here's what I've tried so far - I'm still getting my head around JS so this could be really bad for all I know, but here goes:
roomObjects = $('.object-pos');
var objectCount = 0;
for ( var objectCount = 0; objectCount < 10; objectCount++;) {
roomObjects = rooomObjects[objectCount];
console.log(roomObjects.css("background-position").split(" "));
}
Do you mind sharing why percentages wouldn't work? Usually that's what I would recommend if you're wanting the page to scale correctly on window resizes. I guess if you really wanted to you could do something like:
$(window).resize(function() {
$('#whateverdiv').style.whateverproperty = $('#whateverdiv').style.whateverproperty.toString() + (newPosition - oldPosition);
oldPosition = newPosition;
}
this is obviously not the complete code, but you should be able to fill in the blanks. You'll have to set the oldPosition variable on page load with the original position so that the function works the first time.
edit: you'll also have to strip off the units from the x.style.property string, so that you'll be able to add the value to it
A problem you might well be facing is that when retrieving the current left or top properties, they are returned as a string, with px of % on the end. Try running a parseInt() on the returned values to get a number, then you might well be able to add to the values. Just be sure, when reassigning, that you concatenate "px" or "%" on the end as appropriate.
You could use a bit of jQuery :
var el = $("#id");
var top = el.css("top");
el.css("top", top * 1.2); // increase top by 20%
saves mucking around in the DOM
This might be useful if you want to position things relatively: http://docs.jquery.com/UI/Position

how to get the column count in multi-column layout in webkit

I have some static HTML content. I need to format it using multiple columns, which are then presented as pages to the user. I use something quite simple which boils down to this:
bodyID = document.getElementsByTagName('body')[0];
bodyID.style.width = desiredWidth;
totalHeight = bodyID.offsetHeight;
pageCount = Math.ceil(totalHeight/desiredHeight);
bodyID.style.width = desiredWidth*pageCount;
bodyID.style.height = desiredHeight;
bodyID.style.webkitColumnGap = 0;
bodyID.style.webkitColumnCount = pageCount;
Now, my problem is that webKit honors the height attribute as it should and can create more columns than asked for if the content does not fit into pageCount number of columns.
I need to be able to get the number of columns afterwards to implement paging correctly. However the value of document.getElementsByTagName('body')[0].style.webkitColumnCount does not differ from pageCount even if there are more columns.
Any ideas, how to get the total number of rendered columns?
Thanks in advance.
The solution turned out to be much simpler than I thought. It was just a matter of getting the actual width of the page using bodyID.scrollWidth and then dividing by desiredWidth to get the actual number of pages.
Hope this helps somebody.

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