I am working on a calender.
The structure is simple: The outer div #calender contains all the date-fields with class .field and ID as #DD-MM-YYYY. Now, I want the month name on top ("January") to February when the user scrolls past the #01-02-2015 DIV and so on... So the month name is dynamic.
Question:
HOW do I detect which div is scrolled to?
You can't really know which div is scrolled to. You can compare the original location of a div (relative to document), and see if the scrolled window has reached that location.
window.pageYOffset will give you the number of pixels the document has already scrolled to, whenever it is requested. Now what you'll need is to attach an event listener to the scroll event, give it the relative-to-document-top location of #01-02-2015 div, and checks to see if window.pageYOffset is greater than that. If it is, you can replace the month name. If it's less, you can replace it back.
To know the #01-02-2015 div's offset relative to document, you can jquery's offset() function, or try out the following and let me know if it works:
function getOffsetTop(element){
var offsetTop = 0;
do {
if ( !isNaN( element.offsetTop ) )
offsetTop += element.offsetTop;
} while( element = element.offsetParent );
return offsetTop;
}
(Adapted from finding element's position relative to the document )
** EDIT **
So, as you say the getOffsetTop function works. At page load, you'll want to get the locations/offsets of all the .first div's, and let's assume you also gave them a class or data-id with the specific month. So, let's start by creating an array of these values, like:
var firsts = document.querySelectorAll('.first');
var array = []
for(var i=0;i<firsts.length;i++)
array.push({month:firsts[i].getAttribute('data-id'), offset:getOffsetTop(firsts[i])});
Now you have an array that looks like [{month:'january',offset:214},{month:'february',offset:462}...].
Again, without looking at code, I'm thinking you will need to have a global (or anyway declared outside the scroll function) variable, declared at load time, that stores the index of the current month being looked at (assuming window.pageYOffset is zero then, you can start with array[0]). The scroll function will need to keep checking against the array item before and after this month to see if either of those scroll points are being reached, and if so, change the div content as you need, and also update the currentMonth variable. Something like:
var currentMonth = 0;
window.onscroll = function(){
var index = currentMonth;
if(array[index-1] && window.pageYOffset < array[index-1].offset)){
// change the specific div's innerHTML to that of previous one. You can also target a particular `first` by using document.querySelector('.first[data-id="'+array[index].month+"]'):
currentMonth = currentMonth - 1;
}
else if(array[index+1] && window.pageYOffset > array[index+1].offset)){
// change the div to next one
currentMonth = currentMonth + 1;
}
}
I haven't tested any of this but let me know how you make out, or if the console throws errors.
** EDIT3 **: MDN reference (link) recommends using window.pageYOffset instead of window.scrollY for cross-browser compatibility, so I've updated my answer.
I would calculate it on base of .field height and current scroll of #calendar.
Current scroll of div you can get with the following code:
var position = document.getElementById('calendar').scrollTop;
Assuming you already have your code in place that highlights the next date on scroll, you probably have a handle on the next date that will be highlighted. A simple approach would be on scroll, get the value of the highlighted div (should be the date). If the value === 1, it's the first of a new month so display the next month.
To get the next month, you could store an array of all months and then iterate over the array. If the current iteration is equal to the current month on the calendar, return the next element of the array
function getNextMonth(current_month) {
var month_array = [
'January',
'February',
'March',
'etc'
];
$.each(month_array, function(month, foo) {
if(month == current_month) {
return month_array[($.inArray(current_month, month_array) + 1) % month_array.length];
}
});
}
var calendar = $('calendar');
calendar.hover(function() {
calendar.scroll(function() {
//Your logic to change highlighted div
var current_date = $('#datediv').val(),//However you are selecting your current date div
month = $('#month_div');
if(current_date === 1) {
var next_month = getNextMonth(month);
month.val(next_month);
}
});
});
Honestly though, from a user standpoint it would be odd to scroll through dates on a calendar like that. With most calendars I've seen, scrolling will change the month. That may be a better design method for you unless you have to do it the way you're doing it now.
You could use .offsetTop to check the current y position of each cell, calculate which row is currently shown on top, and then check the current month through its id.
for (var i=0; i < rows.length; i++) {
// Select the last day, incase the first few days are still from the previous month
var lastcell = rows[i].querySelector(".field:last-of-type");
// Check if offsetTop is smaller than 100 (leave some error margin)
if (lastcell.offsetTop < 100) {
var id = lastcell.attribute("id").split("-");
var month = id[1];
// Update month
}
}
Add hidden divs as separators between each month (use css for this). Maybe each month should start on new (css) line. Then you can detect which month you are into by getting the scroll offset of the calendar and see between which separators is in. Assuming you know the offset of each separator or you can loop through with jQuery and the get the one that is closer.
UPDATE:
Between the dates of each month add a hidden separator div e.g <span class="month-separator"></span>. Then these separators by definition will have different offsets (since the dates of a whole month are between each one of them). Then when user scrolls, compute the current offset loop through each separator and see if the offset is close to one of them:
pseudo-code:
var calendarScroll = calendar.pageYOffset; // compute this
var whichMonth = 1; // start with january, i.e 1st month
// for each month separator
$('.month-separator').each(function(index){
// break when encountering the month that is not yet scrolled to
if ($(this).offset().top > calendarScroll)
{
whichMonth = index;
return false; // break from each loop
}
});
You will probably want to put the code above inside the scroll handler like the window.onscroll event handler (e.g see example with sticky header on scroll here).
Related
I have a component (for a visual view):
The current way I render this is like so:
const today = new Date()
const minimumDate = new Date(new Date().setDate(today.getDate() - 14))
const maximumDate = new Date(new Date().setDate(today.getDate() + 14))
const currentDate = minimumDate;
const dateRange = [];
while(currentDate <= maximumDate){
const newDate = new Date(currentDate); newDate.setHours(0,0,0,0)
dateRange.push(newDate)
currentDate.setDate(currentDate.getDate() + 1)
}
const [itemDays, setItemDays] = useState(dateRange)
itemDays.map((date, i) => <CalendarDay key ={i} workout={{}} date={date}/>)
What i want is to render a set amount of days on the calendar, having the entire calendar would cause massive performace hits. The above code does this for +-14 days and the current day. What I need is to convert the scoll bar length into some for of index which corresponds to the dates in itemDays so that when it gets below or above a certain index I can then load the next or previous day continuously.
The trouble is, every method ive tried falls flat. Ive tried these methods which are not accurat enough:
Getting the centre element and try to determine where we are in the array
Using the scroll position and the itemDays length to generate an index (The math never checks out, could be to do with the way scrollLeft works)
Each method was either too far from being a feasible solution or would require working with a bunch of edge cases. I would also like to try to snap the scroll to the centre element which I cant do without getting some form on index like the above mentioned.
Hi I have this working function (have some problems) but I wonder if I can improve on it o change the way of how it works
Explanation:
this function takes text Content of a div compare it to a current day at hand then it does the if on it and show certain icons according if the condition is met or not.
Example
if today is 29.2 the text content of the div is 29 and I want to show the icons only if the day is old so if the date is like future or tomorrow I want the icons to be hidden … the problem that I am facing btw is this logic works until the month switch to new month and the problem is that the new month numbers are like 1.2.3 small numbers so the icons show up because the if looks at bigger than the current day not smaller ..
any idea how to keep them hidden
protentional complex solution
making a hidden span inside every div which will show the day and the month and then make the logic filter according to month/day rather then only day!! too complex right do you have any more simple idea ?
to do this job.
//hide Task controle (archive Trash) if day is older than today
function hideIcons() {
// Extermal lets !!! already defined Trash and Archive Array style too !! FIX!!!
let divs = document.getElementsByClassName('day-number');
let trash = document.getElementsByClassName('fa-trash-alt');
let archive = document.getElementsByClassName('fa-archive');
let today = new Date();
let s = today.getDate()
for (let i = 0; i < divs.length; i++) {
(() => {
if (divs[i].textContent >= s) {
trash[i].style.display = 'none';
archive[i].style.display = 'none';
};
})(i);
}
};
hideIcons();
<span class="day-number"></span>
For a fixed header I add/remove an active class to the anchors like this:
// Store basic variables:
var win = $(window),
sec = $('section'),
nav = $('nav'),
anc = $('nav a'),
pos = nav.offset().top, // Distance of navigation to top of page
arr = sec.map(function(){return $(this).offset().top}).get(); // Distance of each section to top of page in an array
// Make function to add/remove classes:
win.scroll(function(){
var t = win.scrollTop(); // Distance of window top to top of page
t > pos ? nav.addClass('sticky') : nav.removeClass('sticky'), // Compare viewport top with top of navigation
// Compare each section position:
$.each(arr, function(i, val) {
(t >= Math.floor(val) && t < (val + sec.eq(i).outerHeight(true))) ? anc.eq(i-1).addClass('active')
: anc.eq(i-1).removeClass('active')
})
})
On some sections however at the very beginning of the section (i.e. after clicking the anchor and not scrolling further) the active class of the previous section (which is not in viewport anymore) won't get removed. Most probably due to calculations returning significant digits?
How can I get the calculations right so only the current section in viewport gets its anchor highlighted?
While I found some very weird behaviour during debugging this, it's as simple as substracting one pixel from the height of the section:
t >= val && t < (val + sec.eq(i).outerHeight(true) -1) ? ...
The rounding now happens directly inside var arr: return Math.floor($(this).offset().top)
What's really weird here is that even though I rounded everything down in the end the less than condition got returned true even though it mathematically wasn't...
For example this would return true (which apparently isn't):
1824 >= 912 && 1824 < (912 + 912)
So I had to substract 1px to make this true after all.
To me it seems as if jQuery's .outerHeight() prints no significant digits, but does include them. As in the documentation it says:
"The numbers returned by dimensions-related APIs, including .outerHeight(), may be fractional in some cases. Code should not assume it is an integer."
Which gets weird when rounding down doesn't work on it. In my fiddle I put the height of the sections to 912.453125px and .outerHeight() returned 912 but rounding it down with Math.floor() still seemed to return the fractions although it wouldn't print. (See this fiddle, where, when you go to section two and press the debug button, the calculation would be the above example)
So yeah, whatever. I'd like to have a more logical solution but substracting a pixel works.
I wonder if it is possible (in javascript or jquery but without any plugins) to get all elements (for example table rows tr) in current viewport without looping through each of them? I found a lot of examples how to check if specified element is in current viewport, but what I need is a function returns a list of all elements in current viewport. I need this for virtualization because this table should have an infinite capacity and looping through each row from two millions rows is quite inefficient :)
Is there any reasonable way to do this?
Assuming you're not doing anything fancy with positioning, table rows in the viewport can be found with a binary search. For example, for 200000 rows, about 18 lookups are required to locate the first row on the page (jsfiddle, warning: slow to load). This can be extended to find the last element as well, or you could just loop through the elements starting from the first until you find one that is no longer visible.
var rows = table.children().children();
var start = 0;
var end = rows.length;
var count = 0;
while(start != end) {
var mid = start + Math.floor((end - start) / 2);
if($(rows[mid]).offset().top < document.documentElement.scrollTop)
start = mid + 1;
else
end = mid;
}
Obviously this does not work well if anything is floated, absolutely positioned, etc. In short: The nodes being searched must be in order such that rows[N-1].top <= rows[N].top. For something such as a table, this should be true if no styling is applied and no multi-row cells exist.
I'm working on a Javascript/jQuery calendar which includes a month view and a day view. Clicking the days will change the date, which will update the date variables in the day view.
The day view is split up into half hour segments from midnight to 11:00 PM. Clicking on any half hour <tr> (the day view is a table) will create an event between that time clicked and an hour in the future, as well as append a div on top of the calendar, spanning the range of time and positioned at the correct starting point (each pixel is a minute...)
There is a problem, however. If you create an "event" between a certain time span where there is already one in place, they overlap. This is the default behavior, obviously, but what I would like to happen is that if an event is created between a range of dates that is already occupied by an event, they align side by side so that they're not overlapping.
This resembles the behavior seen in the iCal app for mac:
Now my first thought to achieve such a goal was to use collision detection, but all the jQuery plugins for this are bloated or require the elements to be draggable.
Then I thought there might be a way in CSS to do this, where if two elements are overlapping, they split the width evenly.
Then I thought that's ridiculously far fetched, so I'm wondering how I can achieve this as easily as possible.
I'll post the full code in a jsFiddle, but for the most important function would be insertEvent which looks like this:
function insertEvent(start, end){
var end_minutes = new Date(end).getMinutes();
var end_border = new Date(new Date(end).setMinutes(end_minutes + 2));
//$(".day_date").html(start + "<br />" + end);
var diff = Math.abs(end_border - new Date(start));
var minutes = Math.floor((diff/1000)/60);
var start_element = $("td").find("[data-date='" + start + "']");
var offset = start_element.offset().top - $(".second").offset().top;
var this_element = $("<div class='event' style='height:" + minutes + "px;margin-top:" + offset + "px;'></div>");
$(".right").prepend(this_element);
}
This takes two parameters in the javascript new Date() format, one for the start date and one for the end date.
The fiddle: http://jsfiddle.net/charlescarver/HwdwL/
One of the the problems I see with your approach is that there isn't a structure to the storage of the data. I've built a calendar in Javascript before and it's not easy work. First, make sure you have some kind of abstraction for the calendar event. Something like:
function CalendarEvent(startDateTime, endDateTime) {
this.startDateTime = startDateTime;
this.endDateTime = endDateTime;
}
CalendarEvent.prototype.start = function() {
return this.startDateTime.getTime();
};
CalendarEvent.prototype.end = function() {
return this.endDateTime.getTime();
};
CalendarEvent.new = function(startDateTime, endDateTime) {
// This is a little factory method. It prevents calendar events
// from having end times that fall before the start time.
// USE THIS TO INSTANTIATE A NEW CALENDAR EVENT
if(endDateTime.getTime() < startDateTime.getTime()) {
throw new Error("End time falls before start time");
}
return new CalendarEvent(startDateTime, endDateTime);
};
CalendarEvent.compare = function(eventOne, eventTwo) {
// this is a class method to compare two events
// If used with sort it will sort by startDateTime
return eventOne.start() - eventTwo.start();
};
// ... add any other methods you need
Next you're going to want to sort the calendar events. I would sort by start time. Then once it is sorted you can actually re-render everything when changes are made. As long as you sort correctly, determining if a calendar event collides is as simple as this:
CalendarEvent.prototype.intersects = function(otherEvent) {
// If the other event starts after this one ends
// then they don't intersect
if(otherEvent.start() > this.end()) {
return false;
}
// If the other event ends before this one starts
// then they don't intersect
if(otherEvent.end() < this.start()) {
return false;
}
// Everything else is true
return true;
};
Because the data is sorted you know that if two or more calendar events intersect they will have to share the space. Granted, you must think about a few things when you divide the space. Do you want a naive implementation where you just share the space equally from left to right (left having the earliest start time). If so your visual representation could look like this if it had 4 events that shared a space (each block is an event):
However if your events have strange shapes they might cause your calendar to look strange. Consider the following:
In this instance event 2 takes up a lot of vertical space and all the space underneath event 1 is unused. Maybe for a better UX you don't want that kind of thing to happen. If so you should design your rendering algorithm accordingly. Just remember that it is probably easiest to re-render on every change that you encounter, but it's all about how you store the data. If you do not store the data in some kind of structure that is easily traversed then you won't be able to do this kind of thing.
But to complete the answer to your question, here is a fairly naive example. I haven't tested it so this is a pretty big assumption of it working. It is not entirely complete you will have to edit the rendering for yourself. This is merely to give you an idea of how to get it to work. It could definitely look prettier:
function renderCalendarEvents(calendarEvents) {
// Sort the calendar events (assuming calendarEvents is an array)
var sortedEvents = calendarEvents.sort(CalendarEvent.compare);
var index = 0;
// renderEvents is an anonymous function that will be called every time
// you need to render an event
// it returns it's columnDivisor.
var renderEvent = function(position) {
var currentEvent = sortedEvents[index];
var nextEvent = sortedEvents[index + 1];
// The default column divisor is determined by
// the current x-position + 1
var columnDivisor = position + 1;
// Increment before any recursion
index += 1;
// Check if nextEvent even exists
if(nextEvent) {
// If the nextEvent intersects with the current event
// then recurse
if(currentEvent.intersects(nextEvent)) {
// We need to tell the next event that it starts at the
// column position that is immediately +1 to the current event
columnDivisor = renderEvent(position + 1);
}
}
// placeEvent() is some function you can call to actually place
// the calendar event element on the page
// The position is the x-position of the current event
// The columnDivisor is a count of the amount of events sharing this column
placeEvent(currentEvent, position, columnDivisor);
return columnDivisor;
};
while(true) {
// render events until we're done
renderEvent(0);
if(index >= sortedEvents.length) {
break;
}
}
}
Essentially the idea with this particular algorithm is that if the nextEvent on the list exists and that event intersects with the currentEvent then we need to split the width of the currentEvent. It keeps on recursing until it finds no more intersections then it makes it's way back up the chain of recursive calls. I skipped the actual DOM manipulation logic because really the hard part is determining how much you need to split the actual column in order to get these events to fit. So hopefully this all makes a little bit of sense.
EDIT:
To be much more clear, in order to add this to your existing code I would replace your insertEvent function with something like this. I don't write all of the logic for you so you'll have to do some of your own writing. But that's half the fun :-).
function insertEvent(start, end) {
var newEvent = Calendar.new(start, end);
// you'll have to store the array somewhere.
// i'm just assuming some kind of global right now
eventsArray.push(newEvent);
// You'll want to destroy any event elements
destroyCurrentEventElements();
// Now run the rendering function
renderCalendarEvents(eventsArray);
}