I'm trying to create a horizontal timeline but I'm not sure the best way to go about arranging the events divs correctly. At the moment it looks like this:
<div id="events">
<div class="event" style="left:25; position:relative;" id="1">• Entry 1</div>
<div class="event" style="left:25; position:relative;" id="2">• Entry 2</div>
<div class="event" style="left:50; position:relative;" id="3">• Entry 3</div>
<div class="event" style="left:375; position:relative;" id="4">• Entry 4</div>
</div>
I'm trying to make Entry 4 position itself at the top (as there are no divs in the way) but I'm not sure the best solution. It also needs to allow for any number of events overlapping.
Most css options / jQuery plugins don't seem to offer a solution as far as I'm aware they are mostly for flexible grids but this only needs to be flexible vertically and have fixed positions horizontally to line up correctly with the dates.
An obvious first step is to position: absolute and set a top: x but how would one go about checking previous entries to make sure it's not overlapping an older & longer entry. The timeline will hold quite a number of events with various lengths so it can't be too intensive either.
Any suggestions for the best/easiest way to do this?
I suppose you have an events array with start and end dates, then you should check whether events are overlapping by start and end dates. To simulate this, you can check this method:
var events = [{
start: "2018/10/24 15:00",
end: "2018/10/24 18:00"
}, {
start: "2018/10/25 12:00",
end: "2018/10/26 12:00"
}, {
start: "2018/10/25 07:00",
end: "2018/10/25 10:00"
}, {
start: "2018/10/24 12:00",
end: "2018/10/24 20:00"
}, {
start: "2018/10/25 08:00",
end: "2018/10/25 13:00"
}];
var stack = [],
s = 0,
lastStartDate, lastEndDate, newEvents;
events.sort(function(a,b){
if(a.start > b.start) return 1;
if(a.start < b.start) return -1;
return 0;
});
while (events.length > 0) {
stack[s] = [];
newEvents = [];
stack[s].push(events[0]);
lastStartDate = events[0].start;
lastEndDate = events[0].end;
for (var i = 1; i < events.length; i++) {
if (events[i].end < lastStartDate) {
stack[s].push(events[i]);
lastStartDate = events[i].start;
delete events[i];
} else if (events[i].start > lastEndDate) {
stack[s].push(events[i]);
lastEndDate = events[i].end;
}else{
newEvents.push(events[i]);
}
}
events = newEvents;
s++;
}
console.log(stack);
This method picks the first event as the key and checks for other events whether they overlap or not, if they don't overlap, they will be added to the first stack, then if there are any events left, a new stack will be created and then with the remaining events, the code will be run again and a new stack will be prepared again. Until there are any events left in the array, it should continue creating stacks.
Then you can use these stacks to build your events grid. One stack per line.
I used this algorithm in my Evendar plugin. You can check it's grid view.
grid display has a mode, auto-flow : dense, that can do what you want.
In the following example, I have divided every visible column in 4 subcolumns, to allow to set positions in quartes of an hour .
Then, I have classes to set the beginning and end of the events. Of course, you could inject the styles inline and get the same result:
.grid {
margin: 10px;
width: 525px;
border: solid 1px black;
display: grid;
grid-template-columns: repeat(24, 20px);
grid-auto-flow: dense;
grid-gap: 2px;
}
.head {
grid-column: span 4;
background-color: lightblue;
border: solid 1px blue;
}
.elem {
background-color: lightgreen;
}
.start1 {
grid-column-start: 1;
}
.start2 {
grid-column-start: 2;
}
.start3 {
grid-column-start: 3;
}
.start4 {
grid-column-start: 4;
}
.start5 {
grid-column-start: 5;
}
.start6 {
grid-column-start: 6;
}
.end2 {
grid-column-end: 3;
}
.end3 {
grid-column-end: 4;
}
.end4 {
grid-column-end: 5;
}
.end5 {
grid-column-end: 6;
}
.end6 {
grid-column-end: 7;
}
<div class="grid">
<div class="head">1</div>
<div class="head">2</div>
<div class="head">3</div>
<div class="head">4</div>
<div class="head">5</div>
<div class="head">6</div>
<div class="elem start1 end2">A</div>
<div class="elem start2 end5">B</div>
<div class="elem start2 end3">C</div>
<div class="elem start3 end4">D</div>
<div class="elem start3 end3">E</div>
<div class="elem start5 end6">F</div>
<div class="elem start5 end7">G</div>
<div class="elem start6 end8">H</div>
</div>
UPDATE
In addition to inline style, You can also rely on CSS variables in order to easily manage the grid with less of code:
.grid {
margin: 10px;
width: 525px;
border: solid 1px black;
display: grid;
grid-template-columns: repeat(24, 20px);
grid-auto-flow: dense;
grid-gap: 2px;
}
.head {
grid-column: span 4;
background-color: lightblue;
border: solid 1px blue;
}
.elem {
background-color: lightgreen;
grid-column-start: var(--s, 1);
grid-column-end: var(--e, 1);
}
<div class="grid">
<div class="head">1</div>
<div class="head">2</div>
<div class="head">3</div>
<div class="head">4</div>
<div class="head">5</div>
<div class="head">6</div>
<div class="elem" style="--s:1;--e:3">A</div>
<div class="elem" style="--s:5;--e:6">B</div>
<div class="elem" style="--s:1;--e:2">C</div>
<div class="elem" style="--s:3;--e:5">D</div>
<div class="elem" style="--s:1;--e:8">E</div>
<div class="elem" style="--s:3;--e:4">F</div>
<div class="elem" style="--s:4;--e:6">G</div>
<div class="elem" style="--s:2;--e:3">H</div>
</div>
I think this cannot be done with only CSS, so we need to rely on some JS/jQuery.
My idea is to only set the left property within the element, then I loop through all the element to define the top value. I start by setting top to 0 and check if there is already an element in that position.
If not: I place it there and move to next element.
if yes: I increase the top value and check again. I continue until I find a place.
Here a simplified code where I set a random value to left and apply the logic described above.
$('.event').each(function(){
var top=0;
var left = Math.floor(Math.random() *(400));
/* we need to test between left and left+ width of element*/
var e1 = document.elementFromPoint(left, top);
var e2 = document.elementFromPoint(left+80, top);
/* we check only placed element to avoid cycle*/
while ((e1 && e1.classList.contains('placed')) || (e2 && e2.classList.contains('placed'))) {
top += 20; /*Height of an element*/
e1 = document.elementFromPoint(left, top)
e2 = document.elementFromPoint(left+80, top)
}
$(this).css({top:top, // we set the top value
left:left, // we set the left value
zIndex:3// we increase z-index because elementFromPoint consider the topmost element
});
$(this).addClass('placed'); //we mark the element as placed
});
* {
box-sizing: border-box;
}
body {
margin:0;
}
#events {
height: 300px;
width:calc(80px * 6 + 2px);
background:repeating-linear-gradient(to right,blue 0,blue 2px,transparent 2px,transparent 80px);
position:relative;
}
.event {
position: absolute;
width: 80px;
height: 20px;
border: 1px solid #000;
z-index:2;
background:red;
color:#fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="events">
<div class="event" >• Entry 1</div>
<div class="event" >• Entry 2</div>
<div class="event" >• Entry 3</div>
<div class="event" >• Entry 4</div>
<div class="event" >• Entry 5</div>
<div class="event" >• Entry 6</div>
<div class="event" >• Entry 7</div>
<div class="event" >• Entry 8</div>
</div>
You can get something of that effect with just display: grid. Though you will need to be mindful of the order you create the divs.
var grid = document.getElementById("grid")
for (let i = 0; i <= 9; i++)
{
let div = document.createElement("div");
div.innerHTML = i
grid.append(div);
}
let div
div = document.createElement("div");
div.innerHTML = "Entry 1";
div.style.gridColumn = 1
grid.append(div)
div = document.createElement("div");
div.innerHTML = "Entry 4";
div.style.gridColumn = 10
grid.append(div)
div = document.createElement("div");
div.innerHTML = "Entry 2";
div.style.gridColumn = 1
grid.append(div)
div = document.createElement("div");
div.innerHTML = "Entry 3";
div.style.gridColumn = 2
grid.append(div)
#grid {
display: grid;
grid-template-columns: repeat(10, 1fr);
}
<div id="grid" >
</div>
Next is an example of how I would go about it, I have populated events' starting/ending points with random values along a 12h time line. Since the values are random, they might be a little rough around the edges sometimes ;).
Also I assumed two events, A and B, can have the same top if, for instance, A ends at exactly the same time as B or vice versa.
I have commented the code as much as I thought necessary to explain the proceedings.
Hope it helps
$(document).ready(function() {
var timeline = $('#time-line');
for (var i = 0; i < 12; i++)
timeline.append('<div>' + (i + 1) + '</div>')
/*to check event overlapping later*/
var eventData = [];
/*generate random amount of events*/
var eventCount = Math.random() * 10 + 1;
var eventsContainer = $('#events');
var total = 720; //12h * 60min in the example
for (var i = 0; i < eventCount; i++) {
var start = Math.floor(Math.random() * total);
var end = Math.floor(Math.random() * (total - start)) + start;
var duration = end - start;
var event = $('<div class="event" id="' + i + '" >E' + i + '(' + duration + ' min.' + ')</div>');
event.attr('title', 'Start: ' + Math.floor(start / 60) + ':' + (start % 60) + ' | '
+ 'End: ' + Math.floor(end / 60) + ':' + (end % 60));
event.css('width', (duration * 100 / 720) + "%");
event.css('left', (start * 100 / 720) + "%");
var top = getTop(start, end);
event.css('top', top);
/*store this event's data to use it to set next events' top property in getTop()*/
eventsContainer.append(event);
eventData.push([start, end, top, event.height() + 1]); //the +1 is to compensate for the 1px-wide border
}
/**
* Get the event's top property by going through the previous events and
* getting the 'lowest' yet
*
* #param {Number} start
* #param {Number} end
* #returns {Number}
*/
function getTop(start, end) {
var top = 0;
/*for each previous event check for vertical collisions with current event*/
$.each(eventData, function(i, data /*[start, end, top]*/) {
if (data[2] + data[3] > top //if it's height + top is not the largest yet, skip it
//if any of the next 6 conditions is met, we have a vertical collision
//feel free to optimize but tread carefully
&& (data[0] >= start && data[0] < end
|| data[0] < start && data[1] >= end
|| data[0] < start && data[1] > start
|| data[0] < end && data[1] >= end
|| data[0] >= start && data[1] < end
|| data[0] <= start && data[1] > start)) {
top = data[2] + data[3];
}
});
return top;
}
});
#time-line {
width: 100%;
text-align: center
}
#time-line div {
display: inline-block;
width: calc(100% / 12); //12h for the example
text-align: right;
background: lightgray;
}
#time-line div:nth-child(odd) {
background: gray
}
#events {
position: relative
}
#events .event{
position: absolute;
background: lightblue;
text-align: center;
border: 1px solid black
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="time-line"></div>
<div id="events"></div>
Here's a relatively simple function which can be applied to the HTML markup you have. I have hardcoded the height (28px) but the width of the event elements can be variable. The end result is something like this:
A few requirements for this solution to work:
The left property needs to be defined in the HTML markup of each event (as in your example), like:
<div class="event" style="left:25px; position:relative;" id="1">• Entry 1</div>
The events need to be in order, with each event element's left property equal to, or greater, than the previous element's left property
arrangeEvents()
Accepts a selector for the event elements as an argument, then loops through them and applies a top property as needed depending on the end of the longest previous event and start of the current event.
In short, if the start time of the current event is less than the end of a previous event, the current event is positioned below the previous events. If the start of the current event is greater than the end of the previous events, the current event is positioned at the top.
const arrangeEvents = (els) => {
let offset = 1;
let end = 0;
Array.from(els).forEach((event, index) => {
const posLeft = parseInt(event.style.left);
offset = posLeft >= end ? 0 : ++offset;
end = Math.max(end, posLeft + event.offsetWidth);
event.style.top = `-${28 * (index - offset)}px`;
});
}
arrangeEvents(document.querySelectorAll('.event'));
#events {
position: relative;
}
.event {
display: inline-block;
float: left;
clear: both;
border: 1px solid lightgray;
padding: 4px;
height: 28px;
box-sizing: border-box;
}
<div id="events">
<div class="event" style="left:25px; position:relative;" id="1">• Entry 1</div>
<div class="event" style="left:25px; position:relative;" id="2">• Entry 2</div>
<div class="event" style="left:50px; position:relative;" id="3">• #3</div>
<div class="event" style="left:125px; position:relative;" id="4">• A Really Long Entry 4</div>
<div class="event" style="left:175px; position:relative;" id="5">• Entry5</div>
<div class="event" style="left:185px; position:relative;" id="6">• And Even Longer Entry 6</div>
<div class="event" style="left:250px; position:relative;" id="7">• #7</div>
<div class="event" style="left:330px; position:relative;" id="8">• Entry 8</div>
<div class="event" style="left:330px; position:relative;" id="9">• Entry 9</div>
<div class="event" style="left:330px; position:relative;" id="10">• Long Entry 10</div>
<div class="event" style="left:330px; position:relative;" id="11">• Entry 11</div>
<div class="event" style="left:410px; position:relative;" id="12">• Entry 12</div>
<div class="event" style="left:490px; position:relative;" id="13">• Entry 13</div>
</div>
I am trying to get all the HTML elements(div with particular id) which are there 500px below viewport on the page. I want to have this on scroll event.
var windowHeight = window.outerHeight;
var gridTop = windowHeight + 500;
var elements = document.getElementsByClassName('test');
window.addEventListener('scroll', function () {
for (let i = 0; i < elements.length; i++) {
var thisTop = elements[i].offsetTop - document.body.scrollTop;
if (thisTop >= gridTop) {
console.log('hi');
}
}
});
I need help on finding elements 500px below viewport.
EDIT:
I want to do it with pure JavaScript and I am using above code. But every time I am getting thisTop as 0. Please let me know the approach to do this.
Check following solution, here I put parent div which is scrollable.
Note- I have put offset of 50px, in order to support the example.
var parent = document.documentElement
var elements = document.getElementsByClassName('test');
var gridTop = parent.clientHeight + 50;
window.addEventListener('scroll', function() {
var printStr = "";
for (let i = 0; i < elements.length; i++) {
var thisTop = elements[i].offsetTop - parent.scrollTop;
if (thisTop >= gridTop) {
printStr += " "+elements[i].id
}
}
console.clear();
console.log('selected ', printStr);
});
.container div {
width: 40px;
height: 70px;
margin: 5px;
background-color: red;
text-align: center;
}
<div class="container">
<div class="test" id="1">1</div>
<div class="test" id="2">2</div>
<div class="test" id="3">3</div>
<div class="test" id="4">4</div>
<div class="test" id="5">5</div>
<div class="test" id="6">6</div>
<div class="test" id="7">7</div>
<div class="test" id="8">8</div>
<div class="test" id="9">9</div>
<div class="test" id="10">10</div>
<div class="test" id="11">11</div>
<div class="test" id="12">12</div>
</div>
You should not set multiple elements to have the same ID.
It will have a conflict in your js.
If you want to identify a set of DIVs, you should use CLASS instead.
Here, what I did was to find all elements classed as some-class-name iterated through the list,
Then get their width, both style.width and offsetWidth
<div id="the_900px_div" class="some-class-name" style="width:900px;border:1px solid blue;"></div>
<div id="the_200px_div" class="some-class-name" style="width:200px;border:1px solid blue;"></div>
<div id="the_100px_div" class="some-class-name" style="width:100px;border:1px solid red;"></div>
<script type="text/javascript">
function autorun()
{
var elements = document.getElementsByClassName('some-class-name');
var elementsLength = elements.length;
console.log(elements);
for (var i = 0; i < elementsLength; i++){
var sw = elements[i].style.width.replace('px','');
var ow = elements[i].offsetWidth;
var id = elements[i].id;
console.log(sw > 500)
console.log(id + ' style.width is ' + sw + 'px')
console.log(ow > 500)
console.log(id + ' offsetWidth is ' + ow + 'px')
}
}
if (document.addEventListener) document.addEventListener("DOMContentLoaded", autorun, false);
else if (document.attachEvent) document.attachEvent("onreadystatechange", autorun);
else window.onload = autorun;
</script>
Please have a look at my example.
I have multiple rows on my website and a scrollto() button, wich is always at the bottom of the screen.
Depending on where the usere is located on my site at a certain moment, I would like him to move to the next row after he clicked the button.
I am aware of how to make a user scrollto(), but I have no clue what kind of selector I should use.
function myFunction() {
var winScroll = window.scrollTop; // current scroll of window
// find closest div
var rows = document.querySelectorAll('.row');
var closest = rows[0]; // first section
var closest_idx = 0;
var min = closest.offsetTop - winScroll;
rows.forEach(function(row, index) {
var divTopSpace = row.offsetTop - winScroll;
if( divTopSpace < min && divTopSpace > 0 ) {
closest = row;
closest_idx = index;
min = divTopSpace;
}
});
var next_idx = closest_idx + 1;
if (next_idx == rows.length) {
next_idx = 0;
}
console.log(rows[next_idx]);
}
.rowOne {
height: 100vh;
background-color: peachpuff;
}
.rowTwo {
height: 100vh;
background-color: firebrick;
}
.rowThree {
height: 100vh;
background-color: deepskyblue;
}
.btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 30px;
}
<div class="main">
<div class="row rowOne">
<div class="a">
<div class="b">
Foo
</div>
</div>
</div>
<div class="row rowTwo">
<div class="a">
<div class="b">
Bar
</div>
</div>
</div>
<div class="row rowThree">
<div class="a">
<div class="b">
Foobar
</div>
</div>
</div>
<button id="btn" class="btn" onclick="myFunction()">Button</button>
</div>
Thank you in advance.
Since they are all the same height (100% of the window height), the simple solution would be to simply scroll by that amount.
window.scrollBy(0, window.innerHeight);
Otherwise, you'll need to detect which element is the "current" one, and then get it's next sibling, and then scroll to it. Something like this (haven't tested, so syntax might be off, but this should give you an idea)
var winScroll = window.scrollTop; // current scroll of window
// find closest div
var rows = document.querySelectorAll('.row');
var closest = rows[0]; // first section
var closest_idx = 0;
var min = closest.offsetTop - winScroll;
rows.forEach(function(row, index) {
var divTopSpace = row.offsetTop - winScroll;
if( divTopSpave < min && divTopSpave > 0 ) {
closest = row;
closest_idx = index;
min = divTopSpace;
}
});
var next_idx = closest_idx + 1;
if (next_idx == rows.length) {
next_idx = 0;
}
window.scrollTo(rows[next_idx].scrollTop);
I am trying to shift my pictures to the left using jQuery. I am using a while statement to change all pictures. Here is my code:
<script language="javascript">
$(function() {
$("#leftButton").click(function() {
var imglength = $("#content").children("img").length;
var iterations = imglength;
var lastimg = imglength-1;
var nextimg = lastimg-1;
var start = 1;
var text = "";
document.getElementById("Number").innerHTML="The number of pictures is " + imglength;
document.getElementById("Number1").innerHTML="The index of the last image is " + lastimg;
while (start < iterations)
{
var last = $("img:eq('lastimg')").attr("src");
var next = $("img:eq('nextimg')").attr("src");
text += "<br>Iteration: " + imgindex + " and nextimg is:" +nextimg;
$("img:eq('lastimg')").attr('src', next);
start++;
nextimg--;
}
document.getElementById("Number2").innerHTML = text;
})
})
</script>
HTML is here:
Imgae Shift
Left Shift
Pictures
<p id="Number">Number</p>
<p id="Number1">Number</p>
<p id="Number2">Number</p>
<div id="result"></div>
</section>
</section>
<footer>
© Matthew King: 2015, Birmingham, AL 35211
</footer>
</body>
</html>
It might be easier to simply append the first image back to its parent. This should remove it and add it back as the last child. In the demo, each image has a border so you can see what is happening. I am guessing this is what you are after, but if not let me know.
function slideKittens(){
var first = document.querySelector("#kittens > img");
first.parentNode.appendChild(first);
}
setInterval(slideKittens, 1 * 1000);
#kittens img {
border: solid 2px black;
margin: 2px;
padding: 0px;
display: inline-block;
float: left;
}
<div id="kittens" style="overflow: hidden; width:333px; height: 108px">
<img src="http://placekitten.com/100/100" style="border-color: black;" />
<img src="http://placekitten.com/100/100" style="border-color: red;" />
<img src="http://placekitten.com/100/100" style="border-color: green;" />
<img src="http://placekitten.com/100/100" style="border-color: blue;" />
</div>
The problem is that you do not use the variables correctly.
The lines
var last = $("img:eq('lastimg')").attr("src");
var next = $("img:eq('nextimg')").attr("src");
$("img:eq('lastimg')").attr('src', next);
should be
var last = $("img:eq('" + lastimg + "')").attr("src");
var next = $("img:eq('" + nextimg+ "')").attr("src");
$("img:eq('" + lastimg +"')").attr('src', next);
I have and editable div that allow users to enter text. This is part of a chat widget so the design needs the box to be fixed to the bottom.
When a user types I need javascript to catch the resize and append classes to elements where needed.
I managed to get the box to resize upwards but I have had a lot of trouble scaling it back down again.
I have been stuck on this for days now, so any help now would be greatly appreciated.
I have the function and a basic UI version here JSFiddle
Its probably really simple but I am having no luck figuring this out
JSFiddle
var chatBoxSize = {
oldHeight : 0,
scrollHeight : 0,
lastClass : 1,
minClass : 1,
maxClass : 5,
min_height : 0,
last_size : 0,
getClass : function (size){
var sizes = [chatBoxSize.min_height, chatBoxSize.min_height * 2, chatBoxSize.min_height * 3, chatBoxSize.min_height * 4, chatBoxSize.min_height * 5];
switch (size){ case sizes[0] : return 1; break; case sizes[1] : return 2; break; case sizes[2] : return 3; break; case sizes[3] : return 4; break; case sizes[4] : return 5; break; };
//is not exact
var r = null;
console.log(size);
for(var x = 0; x < sizes.length; x++){
if(x < sizes.length){
if(size >= sizes[x] && size < sizes[(x + 1)]){
return (x + 1);
}
}
}
return chatBoxSize.maxClass;
}
};
$(function () {
chatBoxSize.min_height = parseInt($('#msgWriteArea').height());
chatBoxSize.max_height = chatBoxSize.min_height * 4;
chatBoxSize.last_size = chatBoxSize.min_height;
});
function updateChatSize() {
var id = '#msgWriteArea';
var element = document.querySelector(id);
var size = $(id)[0].scrollHeight;
var container = $('.container');
var toRemove = 'size_' + chatBoxSize.lastClass;
console.log(chatBoxSize.getClass(size));
chatBoxSize.lastClass = chatBoxSize.getClass(size);
console.log('Add new class', chatBoxSize.lastClass);
chatBoxSize.last_size = size;
$(id).removeClass(toRemove);
$(id).addClass('size_' + chatBoxSize.lastClass);
container.removeClass(toRemove);
container.addClass('size_' + chatBoxSize.lastClass);
$('#display').val('Removed ' + toRemove + ' Added ' + chatBoxSize.lastClass);
};
$(function (){
$('#msgWriteArea').bind('change keydown input', function () {
if(event.type == 'keydown'){
updateChatSize();
}
});
})
I have thought of just setting the heights to auto but that will not work with the rest of the ui elements.
Sorry to say but No JS needed at all
flex to the rescue, overflow-y: auto; and max-height to the editable DIV:
Heres a jsBin demo so you can play and resize the browser
*{box-sizing:border-box; -webkit-box-sizing:border-box;}
html, body{height:100%; margin:0;font:16px/1 sans-serif; color:#444;}
#chat{
display: flex;
flex-direction: column;
height:100%;
}
#messages{
flex:1;
overflow-y: auto;
}
#messages > div{
padding: 24px;
background: #eef;
margin: 4px 0;
}
#ui{
background: #eee;
}
#msgWriteArea{
padding: 24px;
overflow-y: auto;
height:100%;
background:#ddd;
max-height:100px; /* if max height is exceeded */
overflow-y: auto; /* add scrollbars */
}
<div id="chat">
<div id="messages">
<div>Message 1</div>
<div>Message 2</div>
<div>Message 3</div>
<div>Message 4</div>
<div>Message 5</div>
<div>Message 6</div>
</div>
<div id="ui">
<div id="msgWriteArea" contenteditable>Some text message</div>
</div>
</div>
Now, if you still really need to the the message area height (for some reasons) you could count the number of lines using JS.