Scroll to element with specific text contents - javascript

In the page I have time and right of it some pictures. I wanna to create a button to jump to current time using Tampermonkey. Current time is a text in <h1>. You can see a screenshot below.
How to jump to some position. For example 07:39.
If you want to suggest external library, please show me how to use it in Tampermonkey.

Finding H1 by XPath
If the document tree is stable (the order of elements is always the same) you can get the XPath and get the H1 element by xPath. That would be the fastest (in the mean of CPU time) solution.
Finding H1 with time in it
Otherwise, I'd use regular expression to detect time format in H1 element.
var headers = document.getElementsByTagName("h1");
//Regexp: Any amount of whitespace; One or two numbers; :; Two numbers; any ammount of whitespace
var check = /\s*[0-9]{1,2}:[0-9]{2}\s*/;
//Position
var pos = 0;
for(var i=0, l=headers.length;i<l;i++)
{
if(check.test(headers[i].textContent))
{
pos = headers[i].offsetTop;
//After finding the element, do not loop any more.
break;
}
}
//Scroll to pixel position [0; pos]
window.scrollTo(0,pos);
Getting specific time and finding it
If you want to jump to as specific time (eg. current time at any moment), go for the Date object. With that, you can do as follows:
var date = new Date();
var time_string = zeroFill(date.getHours())+":"+zeroFill(date.getMinutes());
//Procceed with the loop
Using the zeroFill function.

Seems my javascript experience grows :)
var myList = document.getElementsByTagName("h1");
var time = "07:39";
for(var i=0;i<myList.length;i++)
{
if(myList[i].textContent == time)
{
var pos = myList[i].offsetTop;
}
}
window.scrollTo(0,pos);

Related

Search SVG in svg-pan-zoom

So I am using svg-pan-zoom to display a dynamically loaded SVG element. The code for loading the element is similar to the example here: https://ariutta.github.io/svg-pan-zoom/demo/dynamic-load.html (to see what I mean, view the source).
What I am trying to do is search the SVG document for text tags that match a specific query. I found an example here which seems like the solution to that part, but I can't find anything on how to access the SVG content inside svg-pan-zoom.
I'm afraid I don't have any code... I've been doing trial-and-error for quite a while now. Basically I'm just trying to figure out how to access the SVG content so I can search it.
Thanks!
I think this answer can be useful: Pan to specific X and Y coordinates and center image svg-pan-zoom
For example, let's suppose you want to look for a string that is contained inside a tspan (since you are not giving more details), then you can have a search box, and when it changes, the following function is called:
function searchTerm() {
let term = this.value;
var tspans = document.getElementsByTagName("tspan");
var found;
for (var i = 0; i < tspans.length; i++) {
if (tspans[i].innerHTML.includes(term)) {
found = tspans[i];
break;
}
}
let position = found.parentNode.parentNode.getAttribute("transform").split("translate(")[1].split(")")[0];
let posX = position.split(",")[0];
let posY = position.split(",")[1];
panZoom.zoom(1);
panZoom.pan({x:0,y:0});
var realZoom= panZoom.getSizes().realZoom;
panZoom.pan
({
x: -(posX*realZoom)+(panZoom.getSizes().width/2),
y: -(posY*realZoom)+(panZoom.getSizes().height/2)
});
}
As you can see, this code has been prepared for a specific situation, but you can get the position depending on your needs.

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.

Date in Javascript Div loop not updating correctly

I'm trying to generate 3 divs in a for loop and insert the datetime in each iteration . The problem I'm running into is while the function generates the three div's correctly it appends the same time to all 3 div's leaving me (JavaScript newbie) to believe the Date() function is only being executed once . If any we could explain to me what is going on I would greatly appreciate it. Ideally I would like to replace the Date function with graph's and have a graph load in each div.
function gengraphs () {
for (i=0; i < 3; ++i) {
var divTag = document.createElement("div");
divTag.style.width ="1000px";
divTag.style.height ="600px";
divTag.id = "graph"+i;
document.body.appendChild(divTag);
divTag.innerHTML = Date();
// divTag.appendChild(getGraph(divTag));
// divTag.innerHTML = getGraph(divTag);
}
}
The server is executing the script fast enough (in milliseconds) that the date() returned is not visibly different. Try something to delay or use the increment variable i
The loop is being executed so quickly that you won't see a difference in time. Try calling the contents of your loop with a significant delay (e.g. 1s) and put it into a function as shown here: JavaScript sleep/wait before continuing

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!

Improving Efficiency in jQuery function

The while statement in this function runs too slow (prevents page load for 4-5 seconds) in IE/firefox, but fast in safari...
It's measuring pixel width of text on a page and truncating until text reaches ideal width:
function constrain(text, ideal_width){
$('.temp_item').html(text);
var item_width = $('span.temp_item').width();
var ideal = parseInt(ideal_width);
var smaller_text = text;
var original = text.length;
while (item_width > ideal) {
smaller_text = smaller_text.substr(0, (smaller_text.length-1));
$('.temp_item').html(smaller_text);
item_width = $('span.temp_item').width();
}
var final_length = smaller_text.length;
if (final_length != original) {
return (smaller_text + '…');
} else {
return text;
}
}
Any way to improve performance? How would I convert this to a bubble-sort function?
Thanks!
move the calls to $() outside of the loop, and store its result in a temporary variable. Running that function is going to be the slowest thing in your code, aside from the call to .html().
They work very very hard on making the selector engines in libraries fast, but it's still dog slow compared to normal javascript operations (like looking up a variable in the local scope) because it has to interact with the dom. Especially if you're using a class selector like that, jquery has to loop through basically every element in the document looking at each class attribute and running a regex on it. Every go round the loop! Get as much of that stuff out of your tight loops as you can. Webkit runs it fast because it has .getElementsByClassName while the other browsers don't. (yet).
Instead of removing one character at time until you find the ideal width, you could use a binary search.
I see that the problem is that you are constantly modifying the DOM in the loop, by setting the html of the temp_item, and then re reading the width.
I don't know the context of your problem, but trying to adjust the layout by measuring the rendered elements is not a good practice from my point of view.
Maybe you could approach the problem from a different angle. Truncating to a fixed width is common.
Other possibility (hack?) if dont have choices, could be to use the overflow css property of the container element and put the … in other element next to the text. Though i recommend you to rethink the need of solving the problem the way you are intending.
Hugo
Other than the suggestion by Breton, another possibility to speed up your algorithm would be to use a binary search on the text length. Currently you are decrementing the length by one character at a time - this is O(N) in the length of the string. Instead, use a search which will be O(log(N)).
Roughly speaking, something like this:
function constrain(text, ideal_width){
...
var temp_item = $('.temp_item');
var span_temp_item = $('span.temp_item');
var text_len_lower = 0;
var text_len_higher = smaller_text.length;
while (true) {
if (item_width > ideal)
{
// make smaller to the mean of "lower" and this
text_len_higher = smaller_text.length;
smaller_text = text.substr(0,
((smaller_text.length + text_len_lower)/2));
}
else
{
if (smaller_text.length>=text_len_higher) break;
// make larger to the mean of "higher" and this
text_len_lower = smaller_text.length;
smaller_text = text.substr(0,
((smaller_text.length + text_len_higher)/2));
}
temp_item.html(smaller_text);
item_width = span_temp_item.width();
}
...
}
One thing to note is that each time you add something to the DOM, or change the html in a node, the page has to redraw itself, which is an expensive operation. Moving any HTML updates outside of a loop might help speed things up quite a bit.
As other have mentioned, you could move the calls to $() to outside the loop. You can create a reference to the element, then just call the methods on it within the loop as 1800 INFORMATION mentioned.
If you use Firefox with the Firebug plugin, there's a great way of profiling the code to see what's taking the longest time. Just click profile under the first tab, do your action, then click profile again. It'll show a table with the time it took for each part of your code. Chances are you'll see a lot of things in the list that are in your js framework library; but you can isolate that as well with a little trial and error.

Categories