src/angular-material-calendar.js
angular.module("materialCalendar", ["ngMaterial", "ngSanitize"]);
angular.module("materialCalendar").constant("materialCalendar.config", {
version: "0.2.13",
debug: document.domain.indexOf("localhost") > -1
});
angular.module("materialCalendar").config(["materialCalendar.config", "$logProvider", "$compileProvider", function (config, $logProvider, $compileProvider) {
if (config.debug) {
$logProvider.debugEnabled(false);
$compileProvider.debugInfoEnabled(false);
}
}]);
angular.module("materialCalendar").directive("compile", ["$compile", function ($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
);
};
}]);
angular.module("materialCalendar").service("materialCalendar.Calendar", [function () {
function Calendar(year, month, options) {
var now = new Date();
this.setWeekStartsOn = function (i) {
var d = parseInt(i || 0, 10);
if (!isNaN(d) && d >= 0 && d <= 6) {
this.weekStartsOn = d;
} else {
this.weekStartsOn = 0;
}
return this.weekStartsOn;
};
this.setStartDateOfMonth = function (i) {
var d = parseInt(i || 1, 10);
if (!isNaN(d) && d >= 1 && d <= 31) {
this.startDateOfMonth = d;
} else {
this.startDateOfMonth = 1;
}
return this.startDateOfMonth;
};
this.setNoOfDays = function (i) {
var d = parseInt(i || 0, 10);
if (!isNaN(d) && d > 0 ) {
this.noOfDays = d;
} else {
this.noOfDays = 0;
}
return this.noOfDays;
};
this.options = angular.isObject(options) ? options : {};
this.year = now.getFullYear();
this.month = now.getMonth();
this.weeks = [];
this.weekStartsOn = this.setWeekStartsOn(this.options.weekStartsOn);
this.startDateOfMonth = this.setStartDateOfMonth(this.options.startDateOfMonth);
this.noOfDays = this.setNoOfDays(this.options.noOfDays);
this.next = function () {
if (this.start.getMonth() < 11) {
this.init(this.start.getFullYear(), this.start.getMonth() + 1);
return;
}
this.init(this.start.getFullYear() + 1, 0);
};
this.prev = function () {
if (this.month) {
this.init(this.start.getFullYear(), this.start.getMonth() - 1);
return;
}
this.init(this.start.getFullYear() - 1, 11);
};
// Month should be the javascript indexed month, 0 is January, etc.
this.init = function (year, month) {
var now = new Date();
this.year = angular.isDefined(year) ? year : now.getFullYear();
this.month = angular.isDefined(month) ? month : now.getMonth();
var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
var monthLength = daysInMonth[this.month];
// Figure out if is a leap year.
if (this.month === 1) {
if ((this.year % 4 === 0 && this.year % 100 !== 0) || this.year % 400 === 0) {
monthLength = 29;
}
}
// First day of calendar month.
if ( angular.isDefined(this.options.startDateOfMonth) ) {
this.start = new Date(this.year, this.month, this.startDateOfMonth);
} else {
this.start = new Date(this.year, this.month, 1);
}
var date = angular.copy(this.start);
if ( date.getDate() === 1) {
while ( date.getDay() !== this.weekStartsOn) {
date.setDate(date.getDate() - 1);
monthLength++;
}
}
if ( this.noOfDays !== 0) {
while (this.noOfDays % 7 !== 0) {
this.noOfDays++;
}
monthLength = this.noOfDays;
} else {
// Last day of calendar month.
while (monthLength % 7 !== 0) {
monthLength++;
}
}
// Last day of calendar month.
while (monthLength % 7 !== 0) {
monthLength++;
}
this.weeks = [];
for (var i = 0; i < monthLength; ++i) {
// Let's start a new week.
if (i % 7 === 0) {
this.weeks.push([]);
}
// Add copy of the date. If not a copy,
// it will get updated shortly.
this.weeks[this.weeks.length - 1].push(angular.copy(date));
// Increment it.
date.setDate(date.getDate() + 1);
}
};
this.init(year, month);
}
return Calendar;
}]);
angular.module("materialCalendar").service("MaterialCalendarData", [function () {
function CalendarData() {
this.data = {};
this.getDayKey = function(date) {
return [date.getFullYear(), date.getMonth() + 1, date.getDate()].join("-");
};
this.setDayContent = function(date, content) {
this.data[this.getDayKey(date)] = content || "";
};
}
return new CalendarData();
}]);
angular.module("materialCalendar").directive("calendarMd", ["$compile", "$parse", "$templateRequest", "$q", "materialCalendar.Calendar", "MaterialCalendarData", function ($compile, $parse, $templateRequest, $q, Calendar, CalendarData) {
var defaultTemplate = "/* angular-material-calendar.html */";
var injectCss = function () {
var styleId = "calendarMdCss";
if (!document.getElementById(styleId)) {
var head = document.getElementsByTagName("head")[0];
var css = document.createElement("style");
css.type = "text/css";
css.id = styleId;
css.innerHTML = "/* angular-material-calendar.css */";
head.insertBefore(css, head.firstChild);
}
};
return {
restrict: "E",
scope: {
ngModel: "=?",
template: "&",
templateUrl: "=?",
onDayClick: "=?",
onPrevMonth: "=?",
onNextMonth: "=?",
calendarDirection: "=?",
dayContent: "&?",
timezone: "=?",
titleFormat: "=?",
dayFormat: "=?",
dayLabelFormat: "=?",
dayLabelTooltipFormat: "=?",
dayTooltipFormat: "=?",
weekStartsOn: "=?",
tooltips: "&?",
startDateOfMonth: "=?",
noOfDays: "=?",
clearDataCacheOnLoad: "=?",
disableFutureSelection: "=?",
disableSelection: "=?"
},
link: function ($scope, $element, $attrs) {
// Add the CSS here.
injectCss();
var date = new Date();
var month = parseInt($attrs.startMonth || date.getMonth());
var year = parseInt($attrs.startYear || date.getFullYear());
$scope.columnWeekLayout = "column";
$scope.weekLayout = "row";
$scope.timezone = $scope.timezone || null;
$scope.noCache = $attrs.clearDataCacheOnLoad || false;
// Parse the parent model to determine if it's an array.
// If it is an array, than we'll automatically be able to select
// more than one date.
if ($attrs.ngModel) {
$scope.active = $scope.$parent.$eval($attrs.ngModel);
if ($attrs.ngModel) {
$scope.$watch("$parent." + $attrs.ngModel, function (val) {
$scope.active = val;
});
}
} else {
$scope.active = null;
}
// Set the defaults here.
$scope.titleFormat = $scope.titleFormat || "LLLL yyyy";
$scope.dayLabelFormat = $scope.dayLabelFormat || "EEE";
$scope.dayLabelTooltipFormat = $scope.dayLabelTooltipFormat || "EEEE";
$scope.dayFormat = $scope.dayFormat || "d";
$scope.dayTooltipFormat = $scope.dayTooltipFormat || "fullDate";
$scope.dayIdFormat = "dd-MM-yy";
$scope.disableFutureSelection = $scope.disableFutureSelection || false;
$scope.disableSelection = $scope.disableSelection || false;
$scope.sameMonth = function (date) {
var d = angular.copy(date);
return d.getFullYear() === $scope.calendar.year &&
d.getMonth() === $scope.calendar.month;
};
$scope.isDisabled = function (date,startDateOfMonth,noOfDays) {
if (noOfDays!=0 && angular.isDefined(noOfDays)) {
var dateStart = new Date($scope.calendar.year,$scope.calendar.month,startDateOfMonth);
var dateEnd = angular.copy(dateStart);
dateEnd.setDate(dateStart.getDate()+parseInt(noOfDays));
if (date.getDate() <= dateStart && date.getDate() >= dateEnd) { return true; }
}
if ($scope.disableSelection) { return true; }
if ($scope.disableFutureSelection && date > new Date()) { return true; }
return !$scope.sameMonth(date);
};
$scope.calendarDirection = $scope.calendarDirection || "horizontal";
$scope.$watch("calendarDirection", function (val) {
$scope.weekLayout = val === "horizontal" ? "row" : "column";
});
$scope.$watch("weekLayout", function () {
year = $scope.calendar.year;
month = $scope.calendar.month;
bootstrap();
});
var handleCb = function (cb, data) {
(cb || angular.noop)(data);
};
var dateFind = function (arr, date) {
var index = -1;
angular.forEach(arr, function (d, k) {
if (index < 0) {
if (angular.equals(date, d)) {
index = k;
}
}
});
return index;
};
$scope.isActive = function (date) {
var match;
var active = angular.copy($scope.active);
if (!angular.isArray(active)) {
if (active && angular.equals(active.getYear(), date.getYear()) && angular.equals(active.getMonth(), date.getMonth()) && angular.equals(active.getDate(), date.getDate())) {
match = true;
}
} else {
match = dateFind(active, date) > -1;
}
return match;
};
$scope.hasEvents = function (date) {
var data = CalendarData.data[$scope.dayKey(date)];
return (data && data.length > 0);
};
$scope.prev = function () {
$scope.calendar.prev();
var data = {
year: $scope.calendar.year,
month: $scope.calendar.month + 1
};
setData();
handleCb($scope.onPrevMonth, data);
};
$scope.next = function () {
$scope.calendar.next();
var data = {
year: $scope.calendar.year,
month: $scope.calendar.month + 1
};
setData();
handleCb($scope.onNextMonth, data);
};
$scope.handleDayClick = function (date) {
if($scope.disableFutureSelection && date > new Date()) {
return;
}
if($scope.disableSelection) {
return;
}
var active = angular.copy($scope.active);
if (angular.isArray(active)) {
var idx = dateFind(active, date);
if (idx > -1) {
active.splice(idx, 1);
} else {
active.push(date);
}
} else {
if (angular.equals(active, date)) {
active = null;
} else {
active = date;
}
}
$scope.active = active;
if ($attrs.ngModel) {
$parse($attrs.ngModel).assign($scope.$parent, angular.copy($scope.active));
}
handleCb($scope.onDayClick, angular.copy(date));
};
// Small helper function to set the contents of the template.
var setTemplate = function (contents) {
$element.html(contents);
$compile($element.contents())($scope);
};
var init = function () {
$scope.calendar = new Calendar(year, month, {
weekStartsOn: $scope.weekStartsOn || 0,
startDateOfMonth: $scope.startDateOfMonth || 1,
noOfDays: $scope.noOfDays || 0
});
// Allows fetching of dynamic templates via $templateCache.
if ($scope.templateUrl) {
return $templateRequest($scope.templateUrl);
}
return $q.when($scope.template() || defaultTemplate);
};
$scope.dataService = CalendarData;
// Set the html contents of each date.
var getDayKey = function (date) {
return $scope.dataService.getDayKey(date);
};
$scope.dayKey = getDayKey;
var getDayContent = function (date) {
// Initialize the data in the data array.
if ($scope.noCache) {
$scope.dataService.setDayContent(date, "");
} else {
$scope.dataService.setDayContent(date, ($scope.dataService.data[getDayKey(date)] || ""));
}
var cb = ($scope.dayContent || angular.noop)();
var result = (cb || angular.noop)(date);
// Check for async function. This should support $http.get() and also regular $q.defer() functions.
if (angular.isObject(result) && "function" === typeof result.success) {
result.success(function (html) {
$scope.dataService.setDayContent(date, html);
});
} else if (angular.isObject(result) && "function" === typeof result.then) {
result.then(function (html) {
$scope.dataService.setDayContent(date, html);
});
} else {
$scope.dataService.setDayContent(date, result);
}
};
var setData = function () {
angular.forEach($scope.calendar.weeks, function (week) {
angular.forEach(week, getDayContent);
});
};
window.data = $scope.data;
var bootstrap = function () {
init().then(function (contents) {
setTemplate(contents);
setData();
});
};
$scope.$watchGroup(["weekStartsOn","startDateOfMonth","noOfDays"], init);
bootstrap();
// These are for tests, don't remove them..
$scope._$$init = init;
$scope._$$setTemplate = setTemplate;
$scope._$$bootstrap = bootstrap;
}
};
}]);