Sabre Spark

Calendar

Create a calendar from which dates can be selected.

Option name Type Description
module helper/calendar.js
Example
new Calendar(el);

Calendar

function
 Calendar() 

Calendar constructor

Option name Type Description
el Element

Optional

params Object

Optional

function Calendar(el, params) {

  params = params || {};

  // If the first element is an array or array like (NodeList),
  // we will be working with a range.
  if (el && el.hasOwnProperty('length')) {
    params.els = el;
  }
  // If the first argument is a plain object, create a default element
  // since the user MUST provide additional params but the element
  // is optional. Doing it this way to keep the arity the same
  // as other components.
  else if (!(el instanceof HTMLElement)) {
    params = el || {};
    params.els = [this._createDefaultElement()];
  }
  // A single element is passed.
  else {
    params.els = [el];
  }

  params.visibleCounts = params.visibleCounts || [params.visibleCount] || null;
  params.mins = params.mins || [params.min] || null;
  params.maxes = params.maxes || [params.max] || null;
  params.values = params.values || [params.value] || null;

  this._setParams(this.defaults, true);
  this._setParams(params);
  this._parseParams();
  this._bindEventListenerCallbacks();

  // Create a calendar element if we weren't passed one.
  (this.calendarEl ? noop : this._createCalendar).call(this);
}

Calendar.prototype = {

_setParams

property
 _setParams 

Include common functionality.

_setParams: Base.setParams,
_appendChildren: Base.appendChildren,
remove: Base.remove,

_whitelistedParams

property
 _whitelistedParams 

Whitelisted parameters which can be set on construction.

Option name Type Description
_whitelistedParams: ['els', 'visibleCounts', 'advance', 'mins', 'maxes', 'values', 'daysDisabled', 'daysInfo', 'calendarEl', 'viewRange', 'weekStartsOn', 'monthNames', 'monthNamesShort', 'dayInitials', 'dayNames', 'onChange'],

defaults

property
 defaults 

Default values for internal properties we will be setting.
These are set on each construction so we don't leak properties
into the prototype chain.

Option name Type Description
defaults: {
  els: null,
  calendarEl: null,
  visibleCounts: null,
  activeIndex: null,
  mins: null,
  maxes: null,
  values: null,
  daysDisabled: {2016: {3: [9]}},
  daysInfo: {2016: {3: {10: 'A note'}}},
  viewRange: 'month',
  weekStartsOn: 'sunday',
  monthNamesShort: null,
  monthNames: null,
  dayInitials: null,
  dayNames: null,
  advance: true,
  _daysCache: null,
  _weeksCache: null,
  _monthsCache: null,
  _yearsCache: null,
  _onChangeBound: null
},

open

method
 open() 

Open the calendar widget.

Option name Type Description
index Number

Optional The index of the element to tie actions to

open: function(index) {

  index = index || 0;

  // Already open
  if (this.activeIndex === index) {
    return;
  }

  // Set the new index
  this.activeIndex = index;

  // If we don't have a popover yet, create it.
  if (!this.popover) {
    this._createPopover();
  }

  // Create a calendar or calendars inside the popover
  this.render();

  // Finally, show the popover.
  this.popover.open();
},

close

method
 close() 

Close the calendar widget.

close: function() {

  if (this.activeIndex === null || !this.popover) {
    return;
  }

  this.popover.close();

  this.activeIndex = null;
},

getValue

method
 getValue() 

Get the value.

Option name Type Description
asInt Boolean

Get the value as a parsed integer.

return Mixed
getValue: function(asInt) {
  return asInt ? parseInt(this.select.getValue(), 10) : this.select.getValue();
},

setValue

method
 setValue() 

Set the date for a given element.

Option name Type Description
value String, Object
index Number, Element
setValue: function(value, index) {

  if (!value) {
    return;
  }

  var el;

  // If we are passed an element instead of an index, use that.
  if (index instanceof HTMLElement) {
    el = index;
    index = this.els.indexOf(el);
  }
  // Otherwise, find the element in our list.
  else {
    el = this.els[index || 0];
  }

  // Parse the value into an object if we need to.
  value = typeof value === 'object' ? parsedDomFormat.getString(value) : value;

  // Set the value on the input element
  el.value = value;

  // Update the value on the calendar
  this.updateValues(index);
},

updateValues

method
 updateValues() 

Take the date values from the inputs and set them as dates on the calendar.

Option name Type Description
index Number

Optional A single index to update

updateValues: function(index) {

  this.values = this.values || [];

  var els = index === undefined ? this.els : [this.els[index]];
  var i = index || 0;
  var len = (index || 0) + els.length;

  for (; i < len; i++) {
    this.values[i] = this.els[i].value ? parsedDomFormat.getValues(this.els[i].value) : null;
  }
},

render

method
 render() 

Render the calendar or calendars into the popover.

render: function() {

  // If we don't have a popover yet, create it.
  if (!this.popover) {
    this._createPopover();
  }

  // Ensure the cached values are in line with the input values.
  this.updateValues();

  // Get the right number of dates to show based on the viewRange and visibleCounts
  var dates = this._getVisibleDates();

  var content;

  // Create the visible days, weeks, months or years
  if (this.viewRange === 'year')
    content = this._renderYears(dates);
  else if (this.viewRange === 'week')
    content = this._renderWeeks(dates);
  else if (this.viewRange === 'day')
    content = this._renderDays(dates);
  else
    content = this._renderMonths(dates);

  this._appendChildren(this.calendarEl, content, true);
},

_renderYears

method
 _renderYears() 

Render the appropriate number of years.

Option name Type Description
dates Array
return Array
_renderYears: function(

dates

{

   },

_renderWeeks

method
 _renderWeeks() 

Render the appropriate number of weeks.

Option name Type Description
dates Array
return Array
_renderWeeks: function(

dates

{

   },

_renderDays

method
 _renderDays() 

Render the appropriate number of days.

Option name Type Description
dates Array
return Array
_renderDays: function(

dates

{

   },

_renderMonths

method
 _renderMonths() 

Render the appropriate number of months.

Option name Type Description
dates Array
return Array
_renderMonths: function(dates) {

  var months = [];
  var i = 0;
  var len = dates.length;

  for (; i < len; i++) {
    months.push(this._renderMonth(dates[i]));
  }

  return months;
},

_renderMonth

method
 _renderMonth() 

Render a month.

Option name Type Description
date Object
return Element
_renderMonth: function(date) {

  // A unique key for this month used for caching
  var key = date.year + '-' + date.month;

  // Ensure we have a cache
  this._monthsCache = this._monthsCache || [];

  // Return a cached instance
  if (this._monthsCache[key]) {
    return this._monthsCache[key];
  }

  // Create the element
  var el = document.createElement('div');
  el.className = 'spark-calendar__month';

  // Add the title
  el.innerHTML += '<div class="spark-calendar__month-title">' + dates.getMonthName(date.month) + ' ' + date.year + '</div>';

  // Add the days of the week headings
  el.innerHTML += '<div class="spark-calendar__days-of-week">';
  el.innerHTML += this._renderMonthDaysOfWeek();
  el.innerHTML += '</div>';

  // Add the days
  el.innerHTML += '<div class="spark-calendar__days">';
  el.innerHTML += this._renderMonthDays(date);
  el.innerHTML += '</div>';

  // Store in the cache and return
  return (this._monthsCache[key] = el);
},

_renderMonthDaysOfWeek

method
 _renderMonthDaysOfWeek() 

Render the days of week row for a month calendar.

Option name Type Description
return String
_renderMonthDaysOfWeek: function() {

  var daysOfWeek = dates.getDayNames();
  var i = 0;
  var len = daysOfWeek.length;
  var str = '<div class="spark-calendar__days-of-week">';

  for (; i < len; i++) {
    str += '<span class="spark-calendar__day-of-week">' + daysOfWeek[i][0] + '</span>';
  }

  return str += '</div>';
},

_renderMonthDays

method
 _renderMonthDays() 

Render the days of week row for a month calendar.

Option name Type Description
date Object
return String
_renderMonthDays: function(date) {

  var current = dates.getCurrentDate();
  var dayOfWeek = dates.getDayOfWeek({year: date.year, month: date.month, day: 1});
  var startOfWeek = dayOfWeek > 0 ? dates.getWeekStart({year: date.year, month: date.month, day: 1}) : null;
  var monthEnd = dates.getMonthEnd(date);
  var weeks = 6;
  var i = 0;
  var j = 0;
  var str = '<div class="spark-calendar__days">';
  var day = 0;
  var isCurrentMonth = current.year === date.year && current.month === date.month;
  var inactive = startOfWeek;

  for (; i < weeks; i++) {
    for (; j < 7; j++) {

      day = startOfWeek ? startOfWeek.day + j : day + 1;
      str += this._renderMonthDay(day, isCurrentMonth, current, inactive);

      if (startOfWeek && j + 1 >= dayOfWeek) {
        startOfWeek = null;
        inactive = null;
        day = 0;
      }
      else if (day >= monthEnd.day) {
        inactive = true;
        day = 0;
      }
    }
    j = 0;
  }

  return str += '</div>';
},

_renderMonthDay

method
 _renderMonthDay() 

Render a day of the month.

_renderMonthDay: function(day, isCurrentMonth, current, inactive) {

  var str = '<a class="spark-calendar__day';
  str += (isCurrentMonth && current.day === day ? ' spark-calendar__day--today' : '');
  str += (inactive ? ' spark-calendar__day--inactive' : '');
  str += '"';
  str += (inactive || this._isDayDisabled(day, current.month, current.year) ? ' disabled' : '');
  str += ' href="#">';
  str += day ;
  str += this._getDayInfo(day, current.month, current.year);
  str += '</a>';

  return str;
},

_isDayDisabled

method
 _isDayDisabled() 

Is a given day disabled?

Option name Type Description
day Number
month Number
year Number
return Boolean
_isDayDisabled: function(day, month, year) {
  return this.daysDisabled && this.daysDisabled[year] && this.daysDisabled[year][month] && this.daysDisabled[year][month].indexOf(day) !== -1;
},

_getDayInfo

method
 _getDayInfo() 

Get any "info" for a given day.

Option name Type Description
day Number
month Number
year Number
return String
_getDayInfo: function(day, month, year) {
  return this.daysInfo && this.daysInfo[year] && this.daysInfo[year][month] && this.daysInfo[year][month][day] ? this.daysInfo[year][month][day] : '';
},

_createDefaultElement

method
 _createDefaultElement() 

Create the default input element.

Option name Type Description
type String
return Element
_createDefaultElement: function() {
  var el = document.createElement('span');
  el.className = 'spark-input spark-date';
  el.innerHTML = '<input class="spark-input__field" type="date"><span class="spark-label"></span>';
  return el;
},

_createCalendar

method
 _createCalendar() 

Create the calendar.

_createCalendar: function() {
  var el = document.createElement('div');
  el.className = 'spark-calendar';
  el.innerHTML = '<div class="spark-calendar__container"><nav class="spark-calendar__nav"><button class="spark-calendar__previous spark-icon-chevron-left"></button><button class="spark-calendar__next spark-icon-chevron-right"></button></nav></div>';
  this.calendarEl = el;
},

_createPopover

method
 _createPopover() 

Create the popover.

_createPopover: function() {

  this.popover = new Popover(this.els[0], {
    direction: 'bottom',
    contentEl: this.calendarEl
  });
},

_parseParams

method
 _parseParams() 

Parse parameters from the elements.

_parseParams: function() {
  this._parseElsParams();
},

_parseElsParams

method
 _parseElsParams() 

Parse the min, max, value and visible counts from the elements if we can.

Option name Type Description
return Number, Boolean
_parseElsParams: function() {

  if (!this.els) {
    return;
  }

  var i = 0;
  var len = this.els.length;
  var visible = [];
  var mins = [];
  var maxes = [];
  var values = [];

  for (; i < len; i++) {

    if (this.visibleCounts[i])
      visible[i] = this.visibleCounts[i];
    else
      visible[i] = parseInt(this.els[i].getAttribute('data-visible-count'), 10) || 1;

    if (this.mins && this.mins[i])
      mins[i] = typeof this.mins[i] === 'object' ? this.mins[i] : parsedDomFormat.getValues(this.mins[i]);
    else if (this.els[i].getAttribute('min'))
      mins[i] = parsedDomFormat.getValues(this.els[i].getAttribute('min'));

    if (this.maxes && this.maxes[i])
      maxes[i] = typeof this.maxes[i] === 'object' ? this.maxes[i] : parsedDomFormat.getValues(this.maxes[i]);
    else if (this.els[i].getAttribute('max'))
      maxes[i] = parsedDomFormat.getValues(this.els[i].getAttribute('max'));

    if (this.values && this.values[i])
      values[i] = typeof this.values[i] === 'object' ? this.values[i] : parsedDomFormat.getValues(this.values[i]);
    else if (this.els[i].value)
      values[i] = parsedDomFormat.getValues(this.els[i].value);
  }

  this.visibleCounts = visible;
  this.mins = mins;
  this.maxes = maxes;
  this.values = values;
},

_getVisibleDates

method
 _getVisibleDates() 

Get the dates we should be showing. Start with the first value
or today's date.

Option name Type Description
return Array

An array of date objects.

_getVisibleDates: function() {

  var dateArr = [];
  var i = 0;
  var len = this.values.length;
  var method;

  // Get the first month
  for (; i < len && !dateArr.length; i++) {
    if (this.values[i]) {
      dateArr.push(this.values[i]);
    }
  }

  // If we didn't get a month, add today's date.
  if (!dateArr.length) {
    dateArr.push(dates.getCurrentDate());
  }

  // If we are supposed to show more months
  if (dateArr.length < this.visibleCounts[this.activeIndex || 0]) {

    // Add the correct number of visible dates
    for (i = dateArr.length; i < this.visibleCounts[this.activeIndex || 0]; i++) {

      if (this.viewRange === 'year')
        method = dates.getNextYear;
      else if (this.viewRange === 'week')
        method = dates.getNextWeek;
      else if (this.viewRange === 'day')
        method = dates.getNextDay;
      else
        method = dates.getNextMonth;

      dateArr.push(method.call(dates, dateArr[i-1]));
    }
  }

  return dateArr;
},

_bindEventListenerCallbacks

method
 _bindEventListenerCallbacks() 

Create bound versions of event listener callbacks and store them.
Otherwise we can't unbind from these events later because the
function signatures won't match.

_bindEventListenerCallbacks: function() {
  this._onChangeBound = this._onChange.bind(this);
},

_onChange

method
 _onChange() 

When the calendar value changes, run a callback.

_onChange: function() {
  (this.onChange || noop)();
}
  };

  Base.exportjQuery(Calendar, 'Calendar');

  return Calendar;
}));