I have a table that contains a date column. This column has dates from the past to the future including the current time with intervals of approximately 7 min (it may change).
I need to color the row which has present value with green and to move green to the next row when the next time is reached.
Is it possible to listen to the time value and change style over the rows?
I could color the present value but when time goes on, the style doesn't move to the next row since the code executed once on the page load.
Here is my code attempt:
I am using vue so in the HTML table part:
<tbody v-for='row in rows' :key='row.id'>
<tr :style="{'background-color': dateFormat(row.date) ?'green' :'' }">
...
</tr>
</tbody>
and in the methods:
dateFormat(d) {
const time = new Date(d);
const cc = new Date();
if (time.getMinutes() === cc.getMinutes()) return true;
return false;
}
Any help will be appreciated!
Thanks in advance.
There's not too much information of what kind of a table you want to integrate the action to. You can apply the following instructions to achieve what you want.
Make a "model" of the task, at first, read the dates when the active row should change to a JS array (data in the example code). It's handy to also include the table rows in that array, that saves some time when you don't have to query the DOM to find a row to highlight. Then create some variables to store some information of the state of the task (current, next). This information is used to control the state. Finally, create a timer, which runs when ever there's a next date to await. Calculate the delay based on the values you've stored in the model. Something like this:
// Fill the dates (for the example only)
const rows = Array.from(document.querySelector('#traced').rows);
fillDates(new Date(), 5); // constant 5 = increase time [second]
// A date can be passed to Date constructor as an acceptable string too
// Creates the timer
(function() {
const tbody = document.querySelector('#traced'),
rows = Array.from(tbody.rows),
data = rows.map(row => {
const time = new Date(row.cells[1].textContent).getTime();
// The constant 1 above is the date column number of the table
return {row, time};
});
let now = Date.now(),
last = data.length - 1,
next = data.findIndex(row => row.time > now),
current = Math.max(-1, next - 1);
if (now > data[last].time) {
// All the dates are in the past, no need for a timer
data[last].row.classList.add('active');
return;
}
// Updates row highlighting and counters, the timed function
function activateRow() {
// Update highlights
if (current > 0) {
// Remove the current highlight
data[current - 1].row.classList.remove('active');
}
if (current > -1 && next) {
// Highlight the current row
data[current].row.classList.add('active');
}
// Set the timer if needed
if (next > last) {return;} // Quit, no more dates to await
const delay = data[next].time - Date.now();
window.setTimeout(activateRow, delay);
// Update counters
current += 1;
next += 1;
}
activateRow();
}());
// Emulates the server-side dynamic table filling (for the example only)
function fillDates(base, gap = 15000) {
if (gap < 1000) {
gap *= 1000;
}
gap += Math.floor(Math.random() * 3000);
const zone = new Date().getTimezoneOffset(),
date = new Date(base).getTime() - zone * 60000;
rows.forEach((row, index) => {
const dte = new Date(date + gap * index).toISOString(),
end = dte.length - 5;
row.lastElementChild.textContent = dte.substring(0, end);
});
}
.active {
background: green;
}
<table>
<tbody id="traced">
<tr><td>Date 1</td><td></td></tr>
<tr><td>Date 2</td><td></td></tr>
<tr><td>Date 3</td><td></td></tr>
<tr><td>Date 4</td><td></td></tr>
<tr><td>Date 5</td><td></td></tr>
<tr><td>Date 6</td><td></td></tr>
</tbody>
</table>
When you're integrating the example to your own code, include only the IIFE to your JS code, the other parts of the snippet are there to make it possible to run the code reasonably in StackSnippet nevertheless visitor's timezone. Of course you've to define active class in your CSS too. You should also not include the style of the active row on the server, the JS snippet takes care of the highlighting.
Depending on the date format in the table, you might also need to edit the code according to the used format, and even make changes to the actual dates, because depending on visitor's timezone, the dates you add on the server could be badly off on some other timezone, and the automatic highlighter won't work.
There's also a jsFiddle to play with.
Related
I am using an external API to fetch a list of events happening between two dates. I have then used array.reduce to group the events happening on the same day into one array.
const time = events && events.reduce((acc, item) => {
if (!acc[item.fixture.date.split('T')[1]]) {
acc[item.fixture.date.split('T')[1]] = [];
}
acc[item.fixture.date.split('T')[1]].push(item);
return acc;
}, {})
They are labelled by the time in which the event occurs. If I console.log time then you can see in the image below how the data is returned for one day.
Example of returned data
I am trying to work out how to loop through these objects and find the ones that are within 30 minutes of each other. For example: It would find 16:05:00+01:00 and 16:30:00+01:00 and place these into a new array called Interval together.
What would be the easiest way to achieve this?
const datesInRange = (date1, date2, range) => //range = difference in minutes
(date1 - date2) / 1000 / 60 <= range
? true : false
I have an array of objects like this:
[
{
created: "2019-08-14T13:24:36Z",
email: "test1#gmail.com"
},
{
created: "2019-08-15T13:24:36Z",
email: "test2#gmail.com"
},
{
created: "2019-08-16T13:24:36Z",
email: "test1#gmail.com"
},
{
created: "2019-08-22T13:24:36Z",
email: "test4#gmail.com"
},
{
created: "2019-08-22T15:29:66Z",
email: "test1#gmail.com"
}
]
The array is sorted by created. I want to filter those records which are on the last day, irrespective of the time on that day. I added the timestamp using moment.js. Something on these lines:
router.get('/GetLastDayRecords', (req, res) => {
res.json(allRecords.filter(record => record.created.max()));
});
Split the task: first get the maximum date which you'll find at the end of the sorted array (just getting the "YYYY-MM-DD" part of it is enough) and then launch the filter:
let max = allRecords.length ? allRecords[allRecords.length-1].created.slice(0,10) : "";
res.json(allRecords.filter(({created}) => created >= max));
First you need to figure out which day is the last day. If you can assume the records are already sorted, then this is pretty simple:
// Assuming your records are stored in the variable "records"
var lastDay = records[records.length - 1].created;
Now here's where your specific answer may differ based on how you want to handle time zones. Suppose one event happened at 11 PM EST (3 AM GMT) and another event happened at 1 AM EST (5 AM GMT). Are these the same day? In Europe they are, but in America they aren't!
What you need to do is create some cipher from the date+time listed to a "day". This way you can compare two "days" to see if they're the same:
lastDay = new Date(lastDay);
// Setting hours, minutes, and seconds to 0 will give you just the "day" without the time, but by default will use the system timezone
lastDay.setHours(0);
lastDay.setMinutes(0);
lastDay.setSeconds(0);
Once you know which day was the last, it's a simple filter:
// Using a for loop
var results = []
for (var i = 0; i < records.length; i++)
{
if (records[i].created > lastDay) {
results.push(records[i]);
}
}
// Using .filter
var results = records.filter(x => x.created > lastDay);
Alternatively, since we know it's already sorted, we can do it a bit more efficiently by binary searching for the first record on the last day, then grabbing all records after that:
var test = records.length / 2;
var step = records.length / 4;
var found = false;
while (!found) {
if (records[test].created < lastDay) {
test += step;
step /= 2;
}
else if (records[test].created > lastDay) {
if (step == 1) {
// We found the exact cut-off
found = true;
}
else {
test -= step;
step /= 2;
}
}
}
var results = records.slice(test);
Because you're only interested in the "last" day, the logic is a bit simpler. If you wanted the "third" day, you would need to check if created was after the start of the third day and before the end of the third day. We can just check if it's after the start of the last day.
I would create a function to turn your created properties into data be easily compared.
I would also avoid trying to do the entire filter operation in one or two lines as it will difficult to read by other developers.
const dateToInt = date => parseInt( date.split('T').shift().replace(/-/g, '') );
The above will:
Split your created property into an array of date and time.
Select the first element, which happens to be the date.
Remove the dashes in the date.
Coerce the value into a number.
With this you can find the maximum value and filter based on that value.
const nums = foo.map( ({ created }) => dateToInt(created) )
First get a list of numbers from the dataset.
const max = Math.max( ...nums )
Get the biggest number in the list.
const lastDays = foo.filter( ({ created }) => dateToInt(created) === max )
With all that setup, getting the max date is very easy and readable.
Of course, since the list is already sorted. You could have just done this as well.
const last = foo[foo.length -1].created;
const lastDays = foo.filter( ({ created }) => created === last )
I wrote a solution using reduce and filter:
const lastDay = arr.reduce((acc, el) => {
const date = el.created.substr(0,10);
const oldDate = new Date(acc);
const nextDate = new Date(date);
if(oldDate.getTime() > nextDate.getTime()) {
return oldDate;
} else {
return nextDate;
}
}, '1900-01-01');
const lastDayArr = arr.filter(el => {
const date = el.created.substr(0,10);
const oldDate = new Date(lastDay);
const nextDate = new Date(date);
return (oldDate.getTime() === nextDate.getTime());
});
First, you find the most recent date, reducing the original array by comparing which date is the most recent, for this you drop the part of the created string that specifies the hours/minutes/seconds.
You can use a very distant in time date as initial value, or you can set it to null and add another validation in your callback function.
As a second step, you use filter, using the same technique of dropping the hours/minutes/seconds of the created string.
The end result is an array of the elements with the most recent date in your original array.
If you can assume the array is sorted, you can skip the reduce method and just do:
const lastDay = arr[arr.length - 1].created.substr(0,10);
This should work:
allRecords.filter( record => {
let last_date = allRecords[ allRecords.length - 1].created
return last_date.slice(0, 10) === record.created.slice(0, 10)
})
Basically, you are getting the last element from your array and slicing its created value down to its date. Then you are slicing your current record's created value down to its date and comparing if they are the same.
Assuming that the array is already ASC ordered:
const onLastDay = values.filter( v => {
const last = moment(values[ values.length - 1 ].created)
const differenceInDays = last.diff(moment(v.created), 'days')
return differenceInDays < 1
})
console.log(onLastDay)
NOTE: If you try with the reported array you get an error due the fact that the last date is not valid! There are 66 seconds!
I am getting this warning [Vue warn]: You may have an infinite update loop in a component render function. and I think I know why it's happening.
My Use Case :
I have a loop where I am passing an object with index to a vue method. Since computed cannot have arguments so I cannot use it. The remaining option is method.
I am trying to increase the date by one week after the loop reaches to a odd number. Only 1 and 2 is an exception.
Here is my code I have been trying to use
getDate(date, i){
var currentDate=date.startingDate
var res=currentDate
if(i>2){
// change the date
if(i%2==0){
//use previous dates
res=date.startingDate
}else{
// increase the date by one week from the last week
var currDate = new Date(currentDate)
var d=currDate.setDate(currDate.getDate() + 7);
var d=new Date(d)
var newDate=d.toISOString()
var dateArr=newDate.split("T")
var res=dateArr[0]
console.log('ok')
date.startingDate=res // this line is the problem and causes the infinite loop problem.
}
}
return res
}
Here date.startingDate is always a fixed date.
Sample Input
date.startingDate='2018-05-11'
so when i=1, and 2 (i always starts from 1)
out put is date.startingDate='2018-05-11'
when i=3,4 date should increase by one week so expected out put is 2018-05-17
The problem is date.startingDate=res I cannot reset the date to this one. But I have to reset it to the increased new date in order to be able to add new date when i=5,6 or bla bla.
Any suggested solutions in other ways or may be this code can be made better in a different ways? Any help would be highly appreciated.
Thank you so much for the time.
Edit
<div class="row _6-2-1__Tou align-items-center team" v-for="(m,j) in data.row" :key="j">
<div class="col-auto _6-2-1__Tou-one pad-0" >
<ul class="_6-2-1__Tou-text">
<li>{{getDate(m.date,j+1)}}</li>
</ul>
</div>
Ok it seems impossible to avoid this problem. So I changed my approach. I didn't update the date while populating the doom, instead, I used vuex store and getters. When returning from getters, I changed the values there and once all date changes are done, I simply returned the array. It worked nicely. Here is my code
This code is a bit changed in terms of logic as I added dynamic conditions. But concept is same.
fixture(state){
// we can actually update here
for(var i in state.fixture){
var details=state.fixture[i].row
for(var j in details){
//console.log(details[j].date.startingDate)
if(state.counter==state.groupCount){
// increase the date
if(state.date){
var currDate = new Date(state.date)
}else{
var currDate = new Date(details[j].date.startingDate)
}
var d=currDate.setDate(currDate.getDate() + 7);
var d=new Date(d)
var newDate=d.toISOString()
var dateArr=newDate.split("T")
var res=dateArr[0]
details[j].date.startingDate=res
state.date=details[j].date.startingDate
state.counter=1
}else{
if(state.date){
details[j].date.startingDate=state.date
}
state.counter++
}
}
state.counter=0
state.date=''
}
return state.fixture
},
Hope this approach helps others.
I found a way to display multiple countdowns, using javascript.
Having multiple countdown timer events intervals in javascript
I am trying to create a "warmer" display, in a grid. I opted for an HTML table. I have a draft here
I can't get the countdowns to display in the table cells, with separated time components. I just have the entire SPAN tag in the "Days" column.
I'd take a different approach. First of all, instead of creating the table in your HTML, I would store the data about the countdown timers in an array of objects in your JavaScript code and generate the table using JS. Doing this will make it cleaner and more maintainable; to add new items you can just add an object to the array instead of mucking about with HTML.
Secondly, instead of starting an interval for each timer, I would create a single interval that updates all of the timers. Using a single interval means your DOM updates will be batched together and will minimize page reflow.
This code also recalculates the time left each time it updates. If you calculate it once and then just subtract each round, it could introduce drift into your counter. This is because setInterval only guarantees that it will wait at least as many milliseconds as you specify in the delay parameter, it could be more. It probably wouldn't matter much unless your timer was running continuously for a very long time, but over time it would be come inaccurate.
// data is an array of objects, each representing one of your categories.
// Each category has a .title to store its title and a .counters that
// stores an object for each counter in that category.
var data = [
{
title: 'ASTRONOMY',
counters: [
// Each counter has a .title and a .date that is parsed by new Date()
{
title: 'Total Solar Eclipse - (NE US)',
date: 'August 21, 2017'
},
{
title: 'Next Supermoon - Full',
date: 'December 3, 2017'
}
]
},
{
title: 'POLITICS',
counters: [
{
title: 'Next Presidential Election',
date: 'November 3, 2020'
}
]
},
{
title: 'TEST',
counters: [
{
title: '30 seconds from page load',
date: (new Date()).getTime() + (30 * 1000)
},
{
title: 'Unix Epoch',
date: 'January 1, 1970'
}
]
}
];
// this reduce generates the table
let table = data.reduce((acc, category, categoryIndex) => {
return acc + `<tr><td colspan="6" class="category">${category.title}</td></tr>` +
category.counters.reduce((acc, counter, index) => {
return acc + `<tr id="counter-${categoryIndex}-${index}">
<td>${counter.title}</td>
<td>${counter.date}</td>
<td class="days"></td>
<td class="hours"></td>
<td class="minutes"></td>
<td class="seconds"></td>
</tr>`;
}, '');
}, '<table class="countdown"><tr><th>Event</th><th>Date</th><th>Days</th><th>Hours</th><th>Minutes</th><th>Seconds</th></tr>');
table += '</table>';
// insert the table after the noscript tag
document.getElementById('countdown').insertAdjacentHTML('afterend', table);
// generate a flat list of counters
let counters = data.reduce((acc, category, categoryIndex) => {
return acc.concat(category.counters.reduce((counterAcc, counter, index) => {
return counterAcc.concat([{
// counters will be an array of the objects we generate here.
// node contains a reference to the tr element for this counter
node: document.getElementById(`counter-${categoryIndex}-${index}`),
// date is the date for this counter parsed by Date and then converted
// into a timestamp
date: (new Date(counter.date)).getTime()
}]);
}, []));
}, []);
const msSecond = 1000,
msMinute = msSecond * 60,
msHour = msMinute * 60,
msDay = msHour * 24;
let intervalId;
function updateCounters () {
counters.forEach((counter, counterIndex) => {
let remaining = counter.date - Date.now(),
node = counter.node;
let setText = (selector, text) => node.querySelector(selector).textContent = text;
if (remaining > 0) {
setText('.days', Math.floor(remaining / msDay));
remaining %= msDay;
setText('.hours', Math.floor(remaining / msHour));
remaining %= msHour;
setText('.minutes', Math.floor(remaining / msMinute));
remaining %= msMinute;
setText('.seconds', Math.floor(remaining / msSecond));
} else {
// make sure we don't accidentally display negative numbers if a timer
// firing late returns a past timestamp (or the data contains a past date)
setText('.days', 0);
setText('.hours', 0);
setText('.minutes', 0);
setText('.seconds', 0);
// This countdown has reached 0 seconds, stop updating it.
counters.splice(counterIndex, 1);
// no more counters? Stop the timer
if (counters.length === 0) {
clearInterval(intervalId);
}
}
});
}
// display counters right away without waiting a second
updateCounters();
intervalId = setInterval(updateCounters, 1000);
table {
border-collapse: collapse;
}
tr:nth-child(even) {
background-color: #edf;
}
.category {
font-weight: bold;
}
td, th {
padding: .5em;
}
.days, .hours, .minutes, .seconds {
text-align: right;
}
<noscript id="countdown">Sorry, you need JavaScript enabled to view the count
downs</noscript>
More Reading
Creating Accurate Timers in JavaScript
Critical rendering path
Repaints and Reflows: Manipulating the DOM responsibly
If you are ok to use any javascript library why not check FlipClock.js out.
As per the text provided on their site, the following are the logical requirements that were considered when creating the API.
Use as a clock
Use as a timer
Use as a countdown
Themeable using pure CSS
Clean & Dry Syntax
Abstract everything into reusable objects
Full-Featured Developer API to create custom “Clock Faces”
And if you are not ok to use any library here is what you can learn from about how to create a countdown timer using javascript
https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_countdown
There is an outstanding bug in 1.6+ versions of Magento where the % savings for tier prices defaults to 100% when an option is selected. Other contributors have suggested changing product.js around line 747 from
for (var i = 0; i < this.tierPrices.length; i++) {
to be
for (var i = 0; i > this.tierPrices.length; i++) {
This resolves the issue with % savings but that code block is never executed. I am by no means a Javascript expert but this block appears to be updating the tier price and % savings when options are selected. I wanted to find the root of the issue, rather than 'commenting it out'.
From my debugging in Firebug I noticed that the classes for tier price is wrong in product.js and therefore, a tier price of 0 is retrieved, which accounts for why % savings is always 100%.
Firebug shows the price as
class="tier-prices product-pricing">
Buy 10 for
<span class="price">$40.00</span>
whereas product.js is attempting to retrieve the objects using
$$('.price.tier-' + i).each(function (el) {
If you change the above to
$$('.tier-prices .price).each(function (el) {
the tier price is retrieved, but for more than one tier price on a product, there is no way to refer to them individually. The class "price" above does not have a unique identifier or iterative number declared.
Where is class="price" declared for the tier price? In the code of tierprices.phtml it looks like this
<?php echo $this->__('Buy %1$s for %2$s each', $_price['price_qty'], $_price['formated_price'])?>
I've just spent some time on this as it was really starting to bug me after I upgraded a customer's Magento site to 1.7.0.2.
There are two parts to this, I'm going to state the locations and the fixes, but these will not be upgrade proof (for that you will want to create copies of the files and put them in your theme specific folders, although I'm not sure if it's possible with the JS file in question).
1) View Fix
In the file /design/frontend/base/default/template/catalog/product/view/tierprices.phtml
you need to replace lines 32-34
$_product = $this->getProduct();
$_tierPrices = $this->getTierPrices();
$_finalPriceInclTax = $this->helper('tax')->getPrice($_product, $_product->getFinalPrice(), true);
with the following code:
$_product = $this->getProduct();
$_tierPrices = array();
foreach($this->getTierPrices() as $index => $info) {
$_tierPrices[$index] = $info;
$_tierPrices[$index]['formated_price'] = str_replace('class="price"', 'class="price tier-'.$index.'"', $info['formated_price']);
$_tierPrices[$index]['formated_price_incl_tax'] = str_replace('class="price"', 'class="price tier-'.$index.' tier-'.$index.'-incl-tax"', $info['formated_price_incl_tax']);
}
$_finalPriceInclTax = $this->helper('tax')->getPrice($_product, $_product->getFinalPrice(), true);
This fixes the issue with the class not being rendered correctly as you had already figured out. Here is where I found this code - although it didn't fix all the issues, hence the JS changes.
2) JS fix
in the file js/Varien/product.js you need to replace lines 757-769:
$$('.benefit').each(function (el) {
var parsePrice = function (html) {
return parseFloat(/\d+\.?\d*/.exec(html));
};
var container = $(this.containers[3]) ? this.containers[3] : this.containers[0];
var price = parsePrice($(container).innerHTML);
var tierPrice = $$('.price.tier-' + i);
tierPrice = tierPrice.length ? parseInt(tierPrice[0].innerHTML, 10) : 0;
var $percent = Selector.findChildElements(el, ['.percent.tier-' + i]);
$percent.each(function (el) {
el.innerHTML = Math.ceil(100 - ((100 / price) * tierPrice));
});
}, this);
With this:
//
// Code fixed to prevent the redundant inner loop and to use actual tiered pricing in calculation
// It also takes the optional price variants into consideration (eg: +£2 for a blue tshirt)
// Note: I've made this comment deliberately large, to keep the line numbers in sync
//
var parsePrice = function (html) {
return parseFloat(/\d+\.?\d*/.exec(html));
};
var container = $(this.containers[3]) ? this.containers[3] : this.containers[0];
var price = parsePrice($(container).innerHTML);
$$('.percent.tier-' + i).each(function (el) {
el.innerHTML = Math.ceil(100 - ((100 / price) * (this.tierPrices[i] + parseFloat(optionPrices))));
}, this);
I hope this saves at least one person a few hours of their life.
T
The part class="price" is the result from Mage_Directory_Model_Currency::formatPrecision() and executed when $this->formatPrice() or in deeper model layers the price is formatted
with Mage::helper('core')->formatPrice();
You could add the unique identifier by extending (and rewriting) the Mage_Directory_Model_Currency but be aware that formatPrecision is used everywhere. So you could write your own helper/model logic for formatting tier prices.