Skip to content

Instantly share code, notes, and snippets.

@RhysC
Last active November 11, 2016 12:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RhysC/640b2b2a8fb038c6f1b62bbb3059628d to your computer and use it in GitHub Desktop.
Save RhysC/640b2b2a8fb038c6f1b62bbb3059628d to your computer and use it in GitHub Desktop.
Schedule or calendar html page using Knockout bindings, vanilla js and bootstrap shell. View here https://gistpreview.github.io/?640b2b2a8fb038c6f1b62bbb3059628d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calendar Demo</title>
<!-- BEGIN CALENDAR CSS - ideally these would be in a separate file - eg /calendar-template.css -->
<style>
.calendar-header{
width:100%
}
.calendar-header .previous-month-nav {
float:left;
width:50px;
}
.calendar-header .next-month-nav {
float:right;
width:50px;
text-align:right;
}
.calendar-header .calendar-display-date {
margin:0 auto;
text-align:center;
width:90%;
}
table.cal {
width: 100%;
}
table.cal thead tr th, table.cal tbody tr td {
text-align:center;
width: 14.25%;
max-width: 14.25%;
}
table.cal thead tr th {
background-color:#f5f5f5
}
table.cal tbody tr td {
border: #fff solid 2px;
background: #f5f5f5;
margin: 3px 3px;
padding: 10px;
vertical-align:top;
}
.dom{
/* Day of month*/
text-align:right;
padding-right:15px;
}
table.cal tbody tr td.onswing div.svc{
/*background-color: #5cb85c; - darker green */
background-color: #dff0d8;
}
table.cal tbody tr td.offswing div.svc{
/*background-color: #d9534f; - darker red */
background-color: #f2dede;
}
.svc{
margin-top: 5px;
min-height: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-overflow-multiline:ellipsis;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px; /* future proofing */
-khtml-border-radius: 5px; /* for old Konqueror browsers */
}
table.cal tbody tr td.oom {
/* out of month, ie either last month or next month */
background: #fcfcfc;
}
</style>
<!-- END CALENDAR CSS -->
</head>
<body>
<h1>Basic JS calendar</h1>
<p class="lead">Knockout and Moment libraries to build a basic calendar</p>
<monthly-calendar></monthly-calendar>
<!-- http://knockoutjs.com/ -->
<script src="https://ajax.aspnetcdn.com/ajax/knockout/knockout-3.3.0.js"></script>
<!-- note moment is 21kb on the wire, on my shitty mobile connection along with bootstrap it makes it pretty sluggish -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.15.2/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.9/moment-timezone-with-data.min.js"></script>
<!-- START TEMPLATES - ideally these would be in a separate file - eg /calendar-template.html -->
<script type="text/html" id="calendar-template">
<div class="calendar-header">
<div class="previous-month-nav">
<a href="#" data-bind="click: previousMonth"><span data-bind="text: previousMonthText "></span></a>
</div>
<div class="next-month-nav">
<a href="#" data-bind="click: nextMonth"> <span data-bind="text: nextMonthText "></span> </a>
</div>
<div class="calendar-display-date">
<span data-bind="text: displayDate"> </span>
</div>
</div>
<div data-bind="template: { name: 'month-template', data: currentMonth } "></div>
</script>
<script type="text/html" id="month-template">
<table class="cal" cellspacing="0">
<thead>
<tr >
<th>Monday</th>
<th>Tuesday</th>
<th>Wednesday</th>
<th>Thursday</th>
<th>Friday</th>
<th>Saturday</th>
<th>Sunday</th>
</tr>
</thead>
<tbody>
<!-- ko foreach:{data: weeks, as: 'week'} -->
<tr >
<!-- ko template:{name: "week-template", data: week} --><!-- /ko -->
</tr>
<!-- /ko -->
</tbody>
</table>
</script>
<script type="text/html" id="week-template">
<!-- ko template:{name: "day-template", data: monday} --><!-- /ko -->
<!-- ko template:{name: "day-template", data: tuesday} --><!-- /ko -->
<!-- ko template:{name: "day-template", data: wednesday} --><!-- /ko -->
<!-- ko template:{name: "day-template", data: thursday} --><!-- /ko -->
<!-- ko template:{name: "day-template", data: friday} --><!-- /ko -->
<!-- ko template:{name: "day-template", data: saturday} --><!-- /ko -->
<!-- ko template:{name: "day-template", data: sunday} --><!-- /ko -->
</script>
<script type="text/html" id="day-template">
<td data-bind="css:{onswing:events.some(function(e){return e.isOnSite}), oom:!isForCurrentPeriod}">
<div class="dom" data-bind="text:momentDate.date()"></div>
<!-- ko foreach: events -->
<div class="svc">
<div class="shiftstart" data-bind="text:moment(date).tz('Australia/Perth').format('HH:mm')"></div>
<span class="hidden-xs" data-bind="text:service"></span>
</div>
<!-- /ko -->
</td>
</script>
<!-- END TEMPLATES -->
<!-- START ViewModels - ideally these would be in a separate file - eg /calendar-view-models.js -->
<script>
function MonthViewModel(year, month) {
//year is the actual year, ie 2016 where s month is the zero index on the month so december is 11 and jan is 0
var self = this;
var firstDayOfMonth = moment([year, month])
var daysToPrefix = firstDayOfMonth.day() - 1; //ie monday is 1, tuesday is 2 etc we want to show the full week if the 1st is not a monday
var firstDayOfNextMonth = moment([year, month]).add(1, 'month')
var daysToSuffix = (8 - firstDayOfNextMonth.day()) % 7; //ie monday is 1, tuesday is 2 etc we want to show the rest of the week
var startOfCal = moment([year, month]).subtract(daysToPrefix, 'days')
var endOfCal = moment([year, month]).add(1, 'month').subtract(1, 'days').add(daysToSuffix, 'days'); //Note this may just be the last day of themonth or up to 6 days after it
self.weeks = [];
for(var i = startOfCal.isoWeek(); i <= endOfCal.isoWeek(); i++){
self.weeks.push(new WeekViewModel(i, month, year));
}
self.Name = firstDayOfMonth.format('MMMM');//i.e. January
self.Year = year;//i.e. 2016
self.addEvents = function(events){
self.weeks.forEach(function(week){
var weeksEvents = events.filter(function(e){return week.isoWeekNumber === moment(e.date).isoWeek()});
week.addEvents(weeksEvents);
if(weeksEvents.length===0){
console.log("No events for week "+ week.isoWeekNumber)
}
});
}
}
function WeekViewModel(isoWeekNumber, month, year) {
//iso week - 1 being Monday and 7 being Sunday.
// month being the zero based month number
//year being standard gregorian cal year
var self = this;
var currentDate = moment("1-" + isoWeekNumber + "-" + year, "E-WW-YYYY");
self.isoWeekNumber = isoWeekNumber;
var dayVmFactory = function(dayOfWeek){
//dayOfWeek is Iso ie monday is 1 sunday is 7
var date = moment(currentDate).add(dayOfWeek - 1, 'day')
return new DayViewModel(date, date.month() === month);
}
self.monday = dayVmFactory(1);
self.tuesday = dayVmFactory(2);
self.wednesday = dayVmFactory(3);
self.thursday = dayVmFactory(4);
self.friday = dayVmFactory(5);
self.saturday = dayVmFactory(6);
self.sunday = dayVmFactory(7);
self.addEvents = function(events){
self.monday.addEvents(events.filter(function(e){return moment(e.date).isoWeekday()===1}));
self.tuesday.addEvents(events.filter(function(e){return moment(e.date).isoWeekday()===2}));
self.wednesday.addEvents(events.filter(function(e){return moment(e.date).isoWeekday()===3}));
self.thursday.addEvents(events.filter(function(e){return moment(e.date).isoWeekday()===4}));
self.friday.addEvents(events.filter(function(e){return moment(e.date).isoWeekday()===5}));
self.saturday.addEvents(events.filter(function(e){return moment(e.date).isoWeekday()===6}));
self.sunday.addEvents(events.filter(function(e){return moment(e.date).isoWeekday()===7}));
}
}
function DayViewModel(momentDate, isForCurrentPeriod) {
var self = this;
self.isForCurrentPeriod = isForCurrentPeriod;//days of preceeding month and next month can be dull/greyed out etc
self.momentDate = momentDate;
self.events = [];
self.addEvents = function(events){
self.events = [];
events.forEach(function(event){
if(moment(event.date).isSame(self.momentDate, 'day')){
self.events.push(event);
} else{
console.warn({message:"Event is not for the given date", event:event, momentDate:self.momentDate})
}
});
}
}
var MonthlyCalendar = function(){
var self = this;
var now = moment();
self.eventsData = ko.observableArray();
self.selectedMonth = ko.observable(now.month());
self.selectedYear = ko.observable(now.year());
self.addEvents = function(eventsData){
self.eventsData(eventsData);
self.currentMonth().addEvents(eventsData);
}
self.currentMonth = ko.computed(function() {
var vm = new MonthViewModel(self.selectedYear(), self.selectedMonth());
vm.addEvents(eventsData);
return vm;
}).extend({ rateLimit: 1 });// the month and year are both set in the previousMonth and nextMoth functions, which triggers 2 updates to the computed observable, we want to limit that even limiting by one seems to alleviate this
self.previousMonth = function(){
var previous = moment([self.selectedYear(), self.selectedMonth()]).subtract(1, 'month');
self.selectedMonth(previous.month());
self.selectedYear(previous.year());
}
self.nextMonth = function(){
var next = moment([self.selectedYear(), self.selectedMonth()]).add(1, 'month');
self.selectedMonth(next.month());
self.selectedYear(next.year());
}
self.displayDate = ko.computed(function(){
return moment([self.selectedYear(), self.selectedMonth()]).format('MMM YYYY');
});
self.previousMonthText = ko.computed(function(){
return "<< " + moment([self.selectedYear(), self.selectedMonth()]).subtract(1, 'month').format('MMM');
});
self.nextMonthText = ko.computed(function(){
return moment([self.selectedYear(), self.selectedMonth()]).add(1, 'month').format('MMM') + " >>";
});
}
ko.components.register('monthly-calendar', {
viewModel: MonthlyCalendar,
template: { element: 'calendar-template' }
});
</script>
<!-- END ViewModels -->
<script>
var eventsData = [{"date":"2016-10-18T16:00:00+00:00","service":"Annual Leave","isOnSite":false},{"date":"2016-10-19T16:00:00+00:00","service":"Annual Leave","isOnSite":false},{"date":"2016-10-20T16:00:00+00:00","service":"Annual Leave","isOnSite":false},{"date":"2016-10-21T16:00:00+00:00","service":"Annual Leave","isOnSite":false},{"date":"2016-10-22T16:00:00+00:00","service":"Annual Leave","isOnSite":false},{"date":"2016-10-23T16:00:00+00:00","service":"Annual Leave","isOnSite":false},{"date":"2016-10-24T16:00:00+00:00","service":"Annual Leave","isOnSite":false},{"date":"2016-10-25T16:00:00+00:00","service":"Travel Out Not Required","isOnSite":false},{"date":"2016-10-26T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-10-27T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-10-28T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-10-29T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-10-30T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-10-31T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-01T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-02T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-03T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-04T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-05T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-06T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-07T22:15:00+00:00","service":"Travel In","isOnSite":false},{"date":"2016-11-08T22:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-11-09T22:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-11-10T22:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-11-11T22:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-11-12T22:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-11-13T22:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-11-14T22:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-11-16T10:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-11-17T10:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-11-18T10:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-11-19T10:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-11-20T10:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-11-21T10:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-11-22T10:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-11-23T00:35:00+00:00","service":"Travel Out","isOnSite":false},{"date":"2016-11-23T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-24T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-25T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-26T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-27T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-28T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-29T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-11-30T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-01T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-02T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-03T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-05T23:15:00+00:00","service":"Travel In","isOnSite":false},{"date":"2016-12-06T14:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-12-07T14:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-12-08T14:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-12-09T14:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-12-10T14:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-12-11T14:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-12-12T14:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2016-12-13T02:00:00+00:00","service":"Change Over","isOnSite":false},{"date":"2016-12-14T02:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2016-12-15T02:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-12-16T02:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-12-17T02:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-12-18T02:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-12-19T02:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-12-20T02:00:00+00:00","service":"Melbourne","isOnSite":true},{"date":"2016-12-21T01:15:00+00:00","service":"Travel Out","isOnSite":false},{"date":"2016-12-21T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-22T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-23T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-24T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-25T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-26T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-27T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-28T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-29T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-30T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2016-12-31T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-01T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-02T22:15:00+00:00","service":"Travel In","isOnSite":false},{"date":"2017-01-03T21:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2017-01-04T21:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2017-01-05T21:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2017-01-06T21:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2017-01-07T21:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2017-01-08T21:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2017-01-09T21:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2017-01-11T09:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2017-01-12T09:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2017-01-13T09:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2017-01-14T09:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2017-01-15T09:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2017-01-16T09:00:00+00:00","service":"Sydney","isOnSite":true},{"date":"2017-01-17T09:00:00+00:00","service":"Perth","isOnSite":true},{"date":"2017-01-17T16:00:00+00:00","service":"Travel Out","isOnSite":false},{"date":"2017-01-18T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-19T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-20T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-21T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-22T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-23T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-24T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-25T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-26T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-27T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-28T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-29T16:00:00+00:00","service":"OFF","isOnSite":false},{"date":"2017-01-30T16:00:00+00:00","service":"Travel In","isOnSite":false},{"date":"2017-01-31T18:00:00+00:00","service":"Melbourne","isOnSite":true}];
(function(){
vm = new MonthlyCalendar();
vm.addEvents(eventsData);
ko.applyBindings(vm);
})();
</script>
</body>
</html>
@RhysC
Copy link
Author

RhysC commented Nov 10, 2016

The view model is vanilla JS however it does rely on moment.js.
The KO binding could easily be swapped out for other bindings, pick your poison.
Bootstrap is in no way required, it just puts the shell around it, no css or js in the table use any Bootstrap features

@RhysC
Copy link
Author

RhysC commented Nov 11, 2016

Removed Bootstrap - its not required for the demo of this feature set (but does provide a wrapper for the ui)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment