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 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