/** * * How it works from the inside? * * This calendar is used to display certain dates either by specifying range (min and max) * or by specifying exact dates that are available. * * To simplify calculations you need to provide those dates without the needed timezone offset, * instead pretend like they are in current timezone. For example, if you need to specify a minimum * date of "2015-01-08" for a +12 timezone calendar, when providing this date don't provide a * "2015-01-08T00:00:00+12:00" but instead provide a "2015-01-08 00:00:00" as if the timezone you * are using is the local one. This simplifies internal calculations while still gives correct * results. * * However, for "Go To Today" functionality to work correctly, you will need to provide a timezone * you wish your calendar to work with. This timezone is provided via `options.tz`. * * Note: at the moment only UTC timezone is supported and is the one used by default. * */ import frDatepicker from '../../../../public/views/ng/common/frDatepicker.html'; angular.module('fr.datepicker', []).directive('frDatepicker', ['$filter', function ($filter) { return { restrict: 'A', scope: true, template: frDatepicker, link: function (scope, element, attrs) { /** * Initialization */ var options = scope.$eval(attrs.options); // Expose scope. if (options.ctrl) { options.ctrl.scope = scope; } scope.weekly = options.weekly; var onChangeHandler = options.onChange || function () {}; var minDate; var maxDate; // The day that acts as the day that is chosen when clicking "Go to Today", // "Current Week", etc. In other words it's the day that is most relevant in respect // to today. var currentDay = options.currentDay; var years = {}; var months = {}; var days = {}; // Set today as perceived by calendar's timezone. var today = new Date(); today.setHours(0, 0, 0, 0); // Setup calendar accordingly to whether it's configured by exact dates or range. if (options.dates) { setupCalendarByDates(); } else { setupCalendarByRange(); } // Choose Monday as current day for weekly calendar. if (scope.weekly) { setToMonday(currentDay); } // This is the actual date that is selected. scope.date = options.date || currentDay; // This is the currently viewed month. Date may or may not be selected within this month. var viewedMonth = new Date(scope.date); // Establish how "Go To Today" will look. if (scope.weekly) { scope.goToText = 'Current Week'; } else if (options.byDates) { scope.goToText = 'Current Game Day'; } else { scope.goToText = 'Go To Today'; } calculateDates(); /** * Private Methods */ function setupCalendarByDates() { options.byDates = true; // Process each provided date. _(options.dates).each(function (date) { // Build dictionary of all years, months and dates. years[(new Date(date.getFullYear(), 0, 1)).getTime()] = true; months[(new Date(date.getFullYear(), date.getMonth(), 1)).getTime()] = true; days[date.getTime()] = true; // Calculate minimum date from provided dates. if (minDate === undefined || minDate.getTime() > date.getTime()) { minDate = date; } // Calculate maximum date from provided dates. if (maxDate === undefined || maxDate.getTime() < date.getTime()) { maxDate = date; } // If no current date specified then we need to establish one by ourselves. if (!options.currentDay) { if (!currentDay) { currentDay = date; } else if (date.getTime() < currentDay.getTime() && today.getTime() <= date.getTime()) { currentDay = date; } else if (date.getTime() > currentDay.getTime() && today.getTime() >= currentDay.getTime()) { currentDay = date; } } }); } function setupCalendarByRange() { options.byRange = true; minDate = options.minDate; maxDate = options.maxDate; // If no current date specified then we need to establish one by ourselves. if (!options.currentDay) { if (today.getTime() < minDate.getTime()) { currentDay = new Date(minDate); } else if (today.getTime() > maxDate.getTime()) { currentDay = new Date(maxDate); } else { currentDay = today; } } } function calculateYears() { var year; var yearDate; scope.selectedYear = viewedMonth.getFullYear(); // We don't want to overwrite years because it will mess up // change detection, plus there is no need to. if (scope.years) { return; } scope.years = []; for (year = minDate.getFullYear(); year <= maxDate.getFullYear(); year++) { yearDate = new Date(year, 0, 1); if (options.byDates && !years[yearDate.getTime()]) { continue; } scope.years.push(year); } } function calculateMonths() { scope.months = []; var date; for (var i = 0; i <= 11; i++) { date = new Date(viewedMonth.getFullYear(), i, 1); scope.months.push({ date: date, enabled: (options.byDates && months[date.getTime()]) || isDateInRange(date), name: $filter('date')(date, 'MMM'), selected: viewedMonth.getMonth() === i, type: 'month', }); } } function calculateWeeks() { scope.weeks = []; var week; var day; // Calculate date that calendar's 7 by 6 grid will start from. var startingWeekDay = new Date(viewedMonth.getFullYear(), viewedMonth.getMonth(), 1).getDay(); var daysFromPreviousMonth = ((startingWeekDay === 1) ? 7 : (startingWeekDay === 0) ? 6 : startingWeekDay - 1); // Iterate over a 7 by 6 calendar grid. for (var i = 1; i <= 6; i++) { week = { type: 'week', enabled: false, selected: false, days: [], }; for (var j = 1; j <= 7; j++) { // Set date. let date = new Date(viewedMonth); date.setDate(0 - daysFromPreviousMonth + ((i - 1) * 7 + j)); // Get calculated day. day = calculateDay(date); // Enable week if at least one day is enabled. week.enabled = week.enabled || day.enabled; // Select week if at least one day is selected. week.selected = week.selected || day.selected; if (day.date.getDay() === 1) { week.date = day.date; } week.days.push(day); } scope.weeks.push(week); } } function calculateDay(date) { return { date: date, enabled: (options.byDates && days[date.getTime()]) || isDateInRange(date), name: date.getDate(), otherMonth: viewedMonth.getMonth() !== date.getMonth(), selected: scope.date.getTime() === date.getTime(), type: 'day', }; } function calculateDates() { console.time('DatePicker: calculate dates.'); if (scope.calendarOpened) { calculateYears(); calculateMonths(); calculateWeeks(); } setSelectedText(); console.timeEnd('DatePicker: calculate dates.'); } function setYearDate(year) { var lastAvailableDate; var date; for (var i = 0; i < 12; i++) { date = new Date(year, i, 1); if (!(options.byDates && months[date.getTime()]) && !isDateInRange(date)) { continue; } if (lastAvailableDate && viewedMonth.getMonth() < lastAvailableDate.getMonth()) { continue; } lastAvailableDate = date; if (viewedMonth.getMonth() === date.getMonth()) { break; } } viewedMonth = lastAvailableDate; } function selectDate(date) { scope.date = date; viewedMonth = new Date(date); viewedMonth.setDate(1); calculateDates(); scope.notifyDateChange(); } function setSelectedText() { var startDate = scope.date; var endDate; var startFormat = 'd'; var endFormat = 'd MMM, yyyy'; if (scope.weekly) { endDate = new Date(scope.date); endDate.setDate(endDate.getDate() + 6); if (startDate.getMonth() !== endDate.getMonth()) { startFormat = 'd MMM'; } scope.selected = $filter('date')(startDate, startFormat) + ' - ' + $filter('date')(endDate, endFormat); } else { scope.selected = $filter('date')(startDate, endFormat); } } function setToMonday(date) { var day = date.getDay(); date.setDate(date.getDate() - day + (day == 0 ? -6 : 1)); } function isDateInRange(date) { return options.byRange && date.getTime() >= minDate.getTime() && date.getTime() <= maxDate.getTime(); } scope.notifyDateChange = function () { var endDate; if (scope.weekly) { endDate = new Date(scope.date); endDate.setDate(endDate.getDate() + 6); } onChangeHandler(scope.date, endDate); }; /** * Interactions. */ scope.clickDate = function (clicked) { // Don't process invalid clicks. if (clicked.type === 'week' && !scope.weekly || clicked.type === 'day' && scope.weekly || !clicked.enabled) { return; } // Set viewed month. viewedMonth = new Date(clicked.date.getFullYear(), clicked.date.getMonth(), 1); // Set viewed date. if (clicked.type === 'day' || clicked.type === 'week') { scope.date = new Date(clicked.date); scope.notifyDateChange(); } if (scope.weekly) { setToMonday(scope.date); } calculateDates(); }; scope.clickYear = function (year) { setYearDate(year); if (scope.weekly) { setToMonday(scope.date); } calculateDates(); }; scope.clickPrev = function () { var prevDate = new Date(scope.date); while (1) { prevDate.setDate(prevDate.getDate() - 1); // Stop if we hit minimum date. if (prevDate.getTime() < minDate.getTime()) { return; } // If previous condition didn't trigger exit then we are in the range. if (options.byRange) { break; } // Try previous month if this month is not available. if (!months[(new Date(prevDate.getFullYear(), prevDate.getMonth(), 1)).getTime()]) { // Set prevDate to 1st of this month so that // on next iteration it is set to last day of previous month. prevDate.setDate(1); continue; } // Exit the loop if we found available date. if (days[prevDate.getTime()]) { break; } } // Set day to Monday if we have weekly calendar. if (scope.weekly) { setToMonday(prevDate); } selectDate(prevDate); }; scope.clickNext = function () { var nextDate = new Date(scope.date); if (scope.weekly) { nextDate.setDate(nextDate.getDate() + 7 - nextDate.getDay()); } while (1) { nextDate.setDate(nextDate.getDate() + 1); // Stop if we hit maximum date. if (nextDate.getTime() > maxDate.getTime()) { return; } // If previous condition didn't trigger exit then we are in the range. if (options.byRange) { break; } // Try next month if this month is not available. if (!months[(new Date(nextDate.getFullYear(), nextDate.getMonth(), 1)).getTime()]) { // Set nextDate to last day of this month so that // on next iteration it is set to first day of next month. nextDate.setMonth(nextDate.getMonth() + 1); nextDate.setDate(0); continue; } // Exit loop if found available date. if (days[nextDate.getTime()]) { break; } } // Set day to Monday if we have weekly calendar. if (scope.weekly) { setToMonday(nextDate); } selectDate(nextDate); }; scope.clickToday = function () { selectDate(currentDay); }; scope.toggleCalendar = function () { scope.calendarOpened = !scope.calendarOpened; calculateDates(); }; }, }; }]);