Prevent overlapping while positioning element at height of another - javascript

Inside a long text document there are some "special words" to which I want to display notes/annotations on the left. Each note should be as close as possible to the level of the word it is refering to.
The HTML for this is organised in a table. Each paragraph is one table row, consisting on annotations in the left and main text in the right table column. the notes/annotations go to the left. However, unfortunately, there are also some other elements/text nodes in there.
<table>
<tr>
<td class"comments">
<span id="dog" class="note">Note for dog</span>
<span id="cat" class="note">Note for cat</span>
<span id="horse" class="note">Note for horse</span>
Somethin else than a note.
</td>
<td>[Text...]
<span id="dog_anchor" class="reference">Dog</span>
<span id="cat_anchor" class="reference">Cat</span>
<span id="horse_anchor" class="reference">Horse</span>
[Text...]
</td>
</tr>
</table>
It's easy to change the "note"-spans to absolute and positioned them on the level of their reference:
$('span[class*="note"]').each(function (index, value) {
var my_id = $(this).attr('id');
var element_ref = document.getElementById(my_id + "_anchor"); // get reference element
var pos_of_ref = element_ref.offsetTop; // get position of reference element
$(this).css('top', pos_of_ref); // set own position to position of reference element
});
However, life is not so simple here. Since there could be a lot of reference words in one line (while on other there are none of them) I need a rather sophisticated way to distribute the notes so that they are as close as possible to their references without destroying anything in the layout (e.g. being placed outside of the table cell or overlapping with other elements).
Furthermore, the height of the table cells could not be changed. Elements which are not notes must not be moved. (Note elements are always in the order they appear in the main text. That's not the problem.)
So, I need an algorithm like this:
Take all notes in a table cell.
Analyse blank space in that table cell: Which areas are blank, which are blocked?
Distribute the notes in the table cell so that each note is as close as possible to its reference word without any element colliding with any other item in the table cell.
Is there any fast and elegant way to do this without having to write hundreds of lines of code?
Here is a JSfiddle: https://jsfiddle.net/5vLsrLa7/7/
[Update on suggested solutions]
Simply setting the position of the side notes to relative or just moving notes down won't work, because in this case, the side notes will just go downwards relative to their desired position which results in side notes way to far from their reference words. After all, for a neat solution I need to side notes spread in both directions: up and down.
[Update]
The expected result would be something like this:
As you see, it's never possible to place all the notes at the height of their reference. However, the free space is used to position them as close as possible, moving them up and down.

I changed move() function as follows:
function move(){
var prev_offset = 0;
$('span.note').each(function (index, value){
var my_id = $(this).attr('id');
var element_ref = document.getElementById(my_id + "_anchor"); // get reference element
var pos_of_ref = element_ref.offsetTop; // get position of reference element
if (prev_offset >= pos_of_ref){
pos_of_ref = prev_offset + 30;
}
$(this).css('top', pos_of_ref); // set own position to position of reference element
prev_offset = pos_of_ref;
});
}

I'm assuming that your element's notes will be in the correct order always
I made some changes to your javascript:
function move()
{
var arrayTops = [];
$('span[class*="note"]').each(function (index, value)
{
var my_id = $(this).attr('id');
var element_ref = document.getElementById(my_id + "_anchor"); // get reference element
var pos_of_ref = element_ref.offsetTop; // get position of reference element
pos_of_ref = getCorrectTopPosition(arrayTops,pos_of_ref);
$(this).css('top', pos_of_ref); // set own position to position of reference element
arrayTops.push(pos_of_ref);
});
}
function getCorrectTopPosition(arrayTops, newOffsetTop)
{
var notesHeight = 18;
var marginBetweenNotes = 3;
var noteheightWithMargin = notesHeight + marginBetweenNotes;
var lastTop = arrayTops[arrayTops.length-1];
if((lastTop + noteheightWithMargin) >= newOffsetTop)
return lastTop + noteheightWithMargin;
return newOffsetTop;
}

Thanks for all the answers and comments. I was finally able to figure out at least a partical solution which works for me.
First of all, I was able to restructure my HTML, so that now the "non note" elements in the left td are all wrapped in one div which is now the very first element in the td. So, now there is nothing between notes, maybe something before them.
The idea of my solution is not to give the notes a new position but to set a new margin-top to each of them. The maximum amount of margin-top values to be added within a table cell is calculated before (called "roaming space"), being the space below the last note in a table cell. Thus, the table layout is not destroyed.
function move_notes() {
$('tr').each(function (index, value) {
var current_tr = $(this);
var last_app_element_in_tr = $(this).find('span[class*="note"]').last();
if ($(last_app_element_in_tr).length) /* Only preceed if there is at least one note in the table row */ {
var tr_height = $(this).height();
var tr_offset = $(this).offset().top;
var bottom_of_tr = tr_offset + tr_height;
var bottom_of_last_app_el = $(last_app_element_in_tr).offset().top + $(last_app_element_in_tr).height();
var roaming_space = bottom_of_tr - bottom_of_last_app_el; // Calculate the amount of pixels which are "free": The space below the very last note element
$(this).find('span[class*="note"]').each(function (index, value) {
var my_id = $(this).attr('id');
var element_ref = $(current_tr).find("#" + my_id + "_anchor");
var pos_of_ref = $(element_ref).offset().top;
var new_margin_top;
/* Calculate the new margin top: The note should be at the same level as the reference element.
When loading, in most cases the notes are placed too high. So, the margin top of the note should equal
the amount of pixels which the note is "too high". So we subtract the height and the offset of the element
before the current note from the offset of the reference. */
var previous_note = $(this).prev();
// not just notes, but every element in the td in general
if (! $(previous_note).length) // If there is no previous_note, than take the table cell
{
closest_td = $(this).closest("td");
new_margin_top = pos_of_ref - $(closest_td).offset().top;
} else {
new_margin_top = pos_of_ref - $(previous_note).offset().top - $(previous_note).height();
}
var difference_to_previous = $(this).css('marginTop').replace(/[^-\d\.]/g, '') - new_margin_top; // Calculate the difference between the old and the new margin top
if (new_margin_top > 0 && Math.abs(difference_to_previous) > 2) // Only move, if the new margin is greater than zero (no negative margins!) if the difference is greater than 2px (thus preventing ugly "micro moving".
{
var new_roaming_space = roaming_space - difference_to_previous;
if (new_roaming_space > 0) /* if there is still room to move */ {
var margin_top_ready = new_margin_top + "px";
$(this).css('margin-top', margin_top_ready);
roaming_space = new_roaming_space;
} else /* If there is no more space to move: */ {
var margin_top_ready = roaming_space + "px"; // take the rest of the "roaming space" left as margin top
$(this).css('margin-top', margin_top_ready);
return false; // Stop the execution because there is nothing left to do.
}
}
});
}
});
}
window.onload = function () {
move_notes();
};
$(window).resize(function () {
move_notes();
});
As you will notice, one of my main concerns is still not addressed: Notes are only moved down, never up. Because of various problems with my real world webpage I didn't implement that yet. However, an algorith could be something like: If the new margin top is greater than the height of the current note and the difference between the offet of the current note anchor and the following note anchor is less than the height of the current note, than subtract the height of the current note from the new margin.
Still, two problems remain:
If the window is maximized or quickly resized from a rather thin width to a greater width, the adjustment of the note positions won't work. I don't know why.
The performance could be better. As a user, you can see the notes jump down. (Because of strange and unpredictable behaviour in Firefox, I had to move the event handler from document.ready to window.onload)

Related

How to filter elements based on height to fit into a container

Maybe this is more of a maths question, I just don't know how to start calculating.
I have an array of elements(Wave peaks).
These elements come with different heights for different arrays, but the width for each element is 2px with a right margin of 1px.
These elements have to be appended into a container div to form an interactive audio wave. Width of the container is 700px, so only about 233.5 elements can fit into the container at max.
How do I filter the elements to fit into the container by not appending elements with smaller heights so that the elements in the container come close to being >= 232 < 234.
Here's a Fiddle of this example. https://jsfiddle.net/8ko3dozj/
$.create_waves = function(){
$.append = function(h){
$.each(wave_columns, function(index, item){
var peak_height = item*max_peak_remaining_space;
if (item > h){
var wave_peak_div = '<nav class="wave_column_div">\
<div class="wave_column_div_inner" data-height="'+peak_height+'">\
</div>\
</nav>';
$('#wave_result').append(wave_peak_div);
}
});
}
var length = wave_columns.length;
//alert(length);
$.append(0.3);
$.animate_peak_height();
var num_peaks = $('.wave_column_div').length;
if (num_peaks < 233){
//$.append(0.5);
}
}
$.create_waves();
Notes: I don't want to use overflow:hidden for more than 3 elements(peaks) as that will take out the important parts of the audio used for seeking. Taking out low peaks zooms the waveform, as I see it, that's why I prefer it that way.
Here is a possible algorithm:
Create a copy of your wave_columns array.
Sort it in an descending order to get the biggest ones first
Slice it to keep only 233 (or less) waves
The last value is your min_height
In code:
var copy = wave_columns.slice(0,wave_columns.length-1);
copy.sort(function(a, b){return b-a});
copy = copy.slice(0,233);
var min_height = copy[copy.length-1];
JS Fiddle Demo
I'm not sure I understand your question but you can use a counter (maxpeaks) to keep track how many peaks are in the container. When you have enough peaks you can terminate the each loop by returning false.
jsfiddle demo
$.create_waves = function(){
var maxpeaks = 233;
$.append = function(h){
$.each(wave_columns, function(index, item){
if(maxpeaks == 0){
return false;
}
var peak_height = item*max_peak_remaining_space;
if (item > h){
maxpeaks--;
var wave_peak_div = '<nav class="wave_column_div">\
<div class="wave_column_div_inner" data-height="'+peak_height+'">\
</div>\
</nav>';
$('#wave_result').append(wave_peak_div);
}
});
}
var length = wave_columns.length;
//alert(length);
$.append(0.3);
$.animate_peak_height();
var num_peaks = $('.wave_column_div').length;
if (num_peaks < 233){
//$.append(0.5);
}
}

JQuery If Statement using each value from an array

I am writing a function that will be executed on multiple views of an application, and each view can have up to 50 instances of the same element: '.console'. I need to be able to perform an action every time the viewport scrolls to each instance. I have the following code setting up the variables:
//Create empty array with variable values, up to 50
var console = [];
//Find each instance of ".console" and populate the array with its pixel position.
$('.console').each(function() {
console.push($(this)[0].offsetTop);
});
//Determine the current pixel position of the scroll
var scroll = $(document).scrollTop();
Those variables all work fine and dandy, but after hours of pouring over jquery docs I can't figure the if statement out. Here is what I have that works well for the first item in the array:
if (scroll == console[0]){
$('.container').show();
} else {
$('.container').hide();
}
However, I want it to be anytime the scroll position matches each of the values in that array, hopefully something like this:
if (scroll == console[0-50])
Here is the full chunk as is:
$(document).on('scroll', function(){
//Create empty array with variable values, up to 50
var console = [];
//Find each instance of ".console" and populate the array with its pixel position.
$('.console').each(function() {
console.push($(this)[0].offsetTop);
});
//Determine the current pixel position of the scroll
var scroll = $(document).scrollTop();
//Anytime the scroll matches any of the instances of console, show a div
if (scroll == console[0]){
$('.container').show();
} else {
$('.container').hide();
}
});
Any help would be appreciated. I am pretty new to Javascript/JQuery so if I'm approaching the problem in the wrong way altogether, please let me know. Thanks!
Since you said it works for the first one, I'm guessing this may work.
// cache the container
var container = $('.container');
$(document).on('scroll', function(){
//Determine the current pixel position of the scroll
var scroll = $(document).scrollTop();
//Create empty array with variable values, up to 50
var console = [];
//Find each instance of ".console" and populate the array with its pixel position.
$('.console').each(function(index) {
console.push($(this)[0].offsetTop);
if (scroll == console[index]){
$(container).show();
} else {
$(container).hide();
}
});
});
You may wish to take a look at Waypoints. It's a jQuery plugin that is well suited for what you're trying to accomplish.
I whipped up a quick jsFiddle to show it in action: http://jsfiddle.net/dmillz/4xqMb/
$(".console").waypoint(function(direction) {
// Hide or show your ".container" object
});
More Waypoint examples: http://imakewebthings.com/jquery-waypoints/#get-started
Hopefully I understand your problem, which is as follows:
You have a bunch of elements with the .console class, and you want to appear as soon as they are in the viewport. When these elements aren't in the viewport you want them to dissapear?
Since you're interested in when these objects with the .console class are in the viewport, I suggest using this jQuery plugin
http://plugins.jquery.com/appear/
https://github.com/morr/jquery.appear
I suggest wrapping each of the .console objects in a container with another class, and then as these containers appear and disappear show and hide them.
At document ready just do the following:
$(document).ready(function() {
$('<.container-class>').appear();
$('<.container-class>').on('appear', function() { $(this).find('.console').show(); });
$('<.container-class>').on('disappear', function() { $(this).find('.console').hide(); });
});
To answer the question, you could do this:
var cons = $.map($('.console'), function(el) {
return $(el).offset().top;
});
$(document).on('scroll', function(){
var scroll = $(window).scrollTop();
$('.container').toggle( $.inArray(scroll, cons) != -1 );
});
But creating something for a range, considering the height of each element, the height of the window etc. would be a lot more involved.
While the problem was solved via another answer, figuring out how to perform a loop for each value in the array wasn't really solved ... UNTIL NOW!
This is probably a really gross and bloated way to do it, but if you essentially count how many items are in the array, you can then run a loop that many times, putting in the index for each value in the array. Code below:
//Create empty array with variable values
var console = [];
//Find each instance of ".console" and populate the array with its pixel position.
$('.console').each(function() {
console.push($(this)[0].offsetTop);
});
//Count the number of items in the array
var consoleIndex = console.length - 1;
$(document).on('scroll', function(){
//Determine the current pixel position of the scroll
var scroll = $(document).scrollTop();
//Anytime the scroll matches any of the instances of console, show a div
for (var i = 0; i <= consoleIndex; i++) {
if (scroll = console[i]) {
$('.container').toggle();
}
}
});

Can I select an nth css column?

I have a div with 4 css columns and I'd like to select the 3rd and 4th column to make the text slightly darker because I don't have a good contrast between the text and the background-image. Is this possible? I can accept any css or js solution.
Here's the demo.
--EDIT--
It seems that it's not possible to find a selector for pseudo blocks (if I may say) however I still need to figure out a way of creating responsive blocks (like columns) that will split the text equally (in width) whenever the browser is resized.
As far as I know you won't be able to apply styles to the columns.
What you can try however is to use a gradient as a background to make columns 3 and 4 another color.
#columns {
background: -webkit-linear-gradient(left, rgba(0,0,0,0) 50%, blue 50%);
/*... appropriate css for other browser engines*/
}
updated jsFiddle
updated with all browser support gradient
-- EDIT --
Since the intention was actually to change the text color and not the background for the third and fourth column some additional thoughts.
For now it doesn't seem possible to apply styles to single columns inside a container. One possible workaround to change the text color in specific columns is to put every word inside a span. Then to use JavaScript to iterate over the words and determine where a new column starts. Assigning the first element in the third column a new class would make it possible to style this and the following siblings with a different text color.
Because the container is part of a responsive layout and could change in size, the script would have to be re-run on the resize event to account for changing column widths.
The purpose of the code example is to outline how to implement such a solution and should be improved for use in an actual application (e.g. the spans are being re-created every time styleCols is run, lots of console output...).
JavaScript
function styleCols() {
// get #columns
var columns = document.getElementById('columns');
// split the text into words
var words = columns.innerText.split(' ');
// remove the text from #columns
columns.innerText = '';
// readd the text to #columns with one span per word
var spans = []
for (var i=0;i<words.length;i++) {
var span = document.createElement('span');
span.innerText = words[i] + ' ';
spans.push(span);
columns.appendChild(span);
}
// offset of the previous word
var prev = null;
// offset of the column
var colStart = null;
// number of the column
var colCount = 0;
// first element with a specific offset
var firsts = [];
// loop through the spans
for (var i=0;i<spans.length;i++) {
var first = false;
var oL = spans[i].offsetLeft;
console.info(spans[i].innerText, oL);
// test if this is the first span with this offset
if (firsts[oL] === undefined) {
console.info('-- first');
// add span to firsts
firsts[oL] = spans[i];
first = true;
}
// if the offset is smaller or equal to the previous offset this
// is a new line
// if the offset is also greater than the column offset we are in
// (the second row of) a new column
if ((prev === null || oL <= prev) && (colStart === null || oL > colStart)) {
console.info('-- col++', colCount + 1);
// update the column offset
colStart = oL;
// raise the column count
colCount++;
}
// if we have reached the third column
if (colCount == 3) {
// add our new class to the first span with the column offset
// (this is the first span in the current column
firsts[oL].classList.add('first-in-col3');
return;
}
// update prev to reflect the current offset
prev = oL;
}
}
styleCols();
addEventListener('resize', styleCols, false);
CSS
.first-in-col3, .first-in-col3~span {
color: red;
}
jsFiddle
For now i dont think you can do it, here https://bugzilla.mozilla.org/show_bug.cgi?id=371323 is an open bug/request for a feature, you can vote for it. Till then you can consider using tables.
P.S.
Give Up and Use Tables just for the sake of humor :)
The only solution i could think would be a background with your desired color for middle column, customize it for size and position so it goes behind your middle columns and make background-clip:text. Unfortunately it is not supported very well.
You can find more explenations here.

Measuring height of an off page element

I'm Ajaxing in elements. I want to decide where to put them based on their height (think box-packing type algorithm).
When I loop through my elements as such, I always get 0 for the outerHeight:
posts.each(function(i, e) {
var elementHeight = $(e).outerHeight();
// elementHeight is always 0
});
How can I get the display height of my element?
It appears I have to add the element to the page to get the height, which makes sense.
What's the best way of doing this while being invisible to the user as simply as possible?
Append the posts to a hidden (display: none) div element and then iterate over them to get their respective outerHeight values.
HTML
<div id="postContainer"></div>
CSS
#postContainer { display: none;}
JS
var cont = $('#postContainer'), postsLength = posts.length;
posts.each(function(i, e) {
cont.append(e);
if (!--postsLength) calcOuterHeight();
});
function calcOuterHeight(){
cont.find('.posts').each(function(i, e){
var oh = $(e).outerHeight();
})
}
You have to add the elements to the page so that they go through the layout process, then they will have a size.
You can add them to the body, get the height, and then remove them. As the browser is busy running your code while the elements are in the page, the elements will never show up:
var el = $('<div>test</div>');
$(document.body).append(el);
var h = el.height();
el.remove();

Get all HTML elements scrolled into view

I've got a scrollable table in HTML that updates frequently (about once per second) and can contain upwards of 1000 rows. Obviously, it's not reasonable to replace the entire table every time it updates, so I'd like to just replace the table rows that are currently visible.
My first attempt was to just check iterate over all the rows and check their offsets; this works, but it's far too slow to be effective.
What I'm trying to do now is use document.elementFromPoint() to find the topmost element overtop the <tbody>, which is usually a <td> element from where I can get its containing <tr>. This almost works, except in the case where the table itself is obscured by another element (a floating lightbox, for example).
I'm currently looking for either a third solution, or a way to get all elements under a specific point, not just the topmost one. If anyone has any idea how to accomplish either of those, that would be much appreciated.
So I was thinking about this one and came up with this.
http://jsfiddle.net/RKzRE/7/
Instead of monitoring all 1000 rows just monitor a subset of them. Cache their scrolltops on page load for efficiency. So what if you update a few rows above or below the display area? You can find a happy medium between granularity and speed of updates.
I only implemented scrolling down and hardcoded the number of rows to update. But I think the details will be easy to figure out.
//create an object to hold a list of row top locations
var rowmarkers = new Object;
//gather all rows and store their top location
$('tr').each(function(index) {
//create markers for every ten rows
if (index % 10 == 0) {
$(this).addClass('marker');
rowmarkers[$(this).prop('id')] = $(this).offset().top;
}
});
//track whether user scrolls up or down
var prevScrollTop = $(document).scrollTop();
//monitor scroll event
$(document).scroll(function() {
var currentScrollTop = $(this).scrollTop();
if (currentScrollTop > prevScrollTop) {
downScroll(currentScrollTop);
} else {
//up
}
prevScrollTop = currentScrollTop;
});
function downScroll(scrollTop) {
//find the first row that is visible on screen
for (var row in rowmarkers) {
if (rowmarkers[row] > scrollTop) {
//all rows after this can be updated
var updaterow = $('#' + row).prevAll('.marker:first');
if (!updaterow.length) { updaterow = $('.marker:first'); }
//hardcoded # of rows to update
for (var i = 0; i < 20; i++) {
console.log($(updaterow).prop('id'));
updaterow = updaterow.next('tr');
updaterow.not('.marker').addClass('updaterow');
}
return;
}
}
}

Categories

Resources