Last active
November 11, 2016 12:17
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
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
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