Skip to content

Instantly share code, notes, and snippets.

@ZenCocoon
Created July 8, 2015 13:52
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 ZenCocoon/01ff5c681f7899bb24bb to your computer and use it in GitHub Desktop.
Save ZenCocoon/01ff5c681f7899bb24bb to your computer and use it in GitHub Desktop.
BookingSync version for the Bootstrap Datepicker. Adding support for availability map and more
/* ===========================================================
* bootstrap-datepicker.js v1.3.0
* http://twitter.github.com/bootstrap/javascript.html#datepicker
* ===========================================================
* Copyright 2011 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributed by Scott Torborg - github.com/storborg
* Loosely based on jquery.date_input.js by Jon Leighton, heavily updated and
* rewritten to match bootstrap javascript approach and add UI features.
* =========================================================== */
/*
* Updated September 18th, 2012 by Sébastien Grosjean - ZenCocoon
* Updated June 7th, 2013 by Sébastien Grosjean - ZenCocoon - Add data-min support
* Updated September 13th, 2013 by Sébastien Grosjean - ZenCocoon - Add localized format, requires I18n-js
* Updated December 14th, 2013 by Sébastien Grosjean - ZenCocoon - Add data-blocked-days support
* weekly map of the blocked days (starts from Sunday)
* - 0 will leave this day available
* - 1 will block this day
* example: data-blocked-days="1000001" (will block Sunday and Saturday)
* Updated December 14th, 2013 by Sébastien Grosjean - ZenCocoon - Add data-availability-map support
* - 0 stands for available
* - 1 (or more) stands for unavailable
* example: data-availability-map="0011210"
* Updated December 3rd, 2014 by Sébastien Grosjean - ZenCocoon - Add new input to properly support
* localized date formats, your date input need a data-value="2015-12-25" for proper initialization.
* Updated December 10th, 2014 by Sébastien Grosjean - ZenCocoon - Require Availability Map
* to use a data-availability-start_date as well
* Updated January 21th, 2015 by Karol Galanciak - Azdaroth and Sébastien Grosjean - ZenCocoon -
* Add support for selectable tentatives
*/
!function ( $ ) {
var STANDALONE = false;
var selector = '[data-datepicker]',
all = [];
function clearDatePickers(except) {
var ii;
for(ii = 0; ii < all.length; ii++) {
if(all[ii] != except) {
all[ii].hide();
}
}
}
function DatePicker( element, options ) {
this.$el = $(element);
this.proxy('show').proxy('ahead').proxy('hide').proxy('keyHandler').proxy('selectDate');
var options = $.extend({}, $.fn.datepicker.defaults, options );
if((!!options.parse) || (!!options.format) || !this.detectNative()) {
$.extend(this, options);
this.$el.data('datepicker', this);
all.push(this);
this.init();
}
}
DatePicker.prototype = {
detectNative: function(el) {
if (STANDALONE) return false;
// Attempt to activate the native datepicker, if there is a known good
// one. If successful, return true. Note that input type="date"
// requires that the string be RFC3339, so if the format/parse methods
// have been overridden, this won't be used.
if(navigator.userAgent.match(/(iPad|iPhone); CPU(\ iPhone)? OS 5_\d/i)) {
// jQuery will only change the input type of a detached element.
var $marker = $('<span>').insertBefore(this.$el);
this.$el.detach().attr('type', 'date').insertAfter($marker);
$marker.remove();
return true;
}
return false;
}
, init: function() {
var $months = this.nav('months', 1);
var $years = this.nav('years', 12);
var $nav = $('<div>').addClass('nav').append($months).append($years);
var $min = this.$el.data('min');
var $blockedDays = this.$el.data('blocked-days');
var $availabilityMap = this.$el.data('availability-map');
var $availabilityStartDate = this.$el.data('availability-start-date');
var $tentativesAsAvailable = this.$el.data('tentatives-as-available');
var $calendar = $("<div>").addClass('calendar');
this.$month = $months.find('.name');
this.$year = $years.find('.name');
this.createApiInput();
if (typeof($blockedDays) !== "undefined") {
this.$blockedDays = String($blockedDays);
}
if (typeof($availabilityMap) !== "undefined") {
this.$availabilityMap = String($availabilityMap);
}
if (typeof($availabilityStartDate) !== "undefined") {
this.$availabilityStartDate = this.parse($availabilityStartDate);
}
if (typeof($tentativesAsAvailable) !== "undefined") {
this.$tentativesAsAvailable = $tentativesAsAvailable;
}
if ($min && $min.length == 10) {
this.$min = this.parse(this.$el.data('min'));
}
// Populate day of week headers, realigned by startOfWeek.
for (var i = 0; i < this.shortDayNames.length; i++) {
$calendar.append('<div class="dow">' + this.shortDayNames[(i + this.startOfWeek) % 7] + '</div>');
};
if (this.$tentativesAsAvailable) {
var $legend = $('<div class="legend">')
.append($('<p class="legend-item-wrapper">')
.append($('<span class="legend-item tentative">')).append(I18n.t('datepicker.legend.tentative'))
);
} else {
var $legend = ""
}
this.$days = $('<div></div>').addClass('days');
$calendar.append(this.$days);
this.$picker = $('<div></div>')
.click(function(e) { e.stopPropagation() })
// Use this to prevent accidental text selection.
.mousedown(function(e) { e.preventDefault() })
.addClass('datepicker')
.append($nav)
.append($calendar)
.append($legend)
.insertAfter(this.$el);
this.$el.change($.proxy(function() { this.selectDate(); }, this));
this.selectDate();
if (STANDALONE) {
this.$el.hide();
this.show();
} else {
this.$el
.focus(this.show)
.click(this.show)
this.hide();
if (/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) {
this.$el.attr('readonly', 'readonly').addClass('prevent-keyboard');
}
}
}
// This overwrite the input to force the value to be in %Y-%m-%d format.
// Required for localized date support
, createApiInput: function() {
var inputName = this.$el.attr('name');
var inputHTML = '<input type="hidden" name="' + inputName + '">';
this.$elApiInput = $(inputHTML).insertAfter(this.$el);
this.$elApiInput.val(this.$el.attr('data-value'));
}
, nav: function( c, months ) {
var $subnav = $('<div>' +
'<span class="prev button">&larr;</span>' +
'<span class="name"></span>' +
'<span class="next button">&rarr;</span>' +
'</div>').addClass(c)
$('.prev', $subnav).click($.proxy(function() { this.ahead(-months, 0) }, this));
$('.next', $subnav).click($.proxy(function() { this.ahead(months, 0) }, this));
return $subnav;
}
, updateName: function($area, s) {
// Update either the month or year field
$area.html(s);
}
, selectMonth: function(date) {
var newMonth = new Date(date.getFullYear(), date.getMonth(), 1);
this.$today = new Date();
if (!this.curMonth || !(this.curMonth.getFullYear() == newMonth.getFullYear() &&
this.curMonth.getMonth() == newMonth.getMonth())) {
this.curMonth = newMonth;
var rangeStart = this.rangeStart(date), rangeEnd = this.rangeEnd(date);
var num_days = this.daysBetween(rangeStart, rangeEnd);
this.$days.empty();
for (var ii = 0; ii <= num_days; ii++) {
var thisDay = new Date(rangeStart.getFullYear(), rangeStart.getMonth(), rangeStart.getDate() + ii, 12, 00);
var $day = $('<div></div>').attr('date', this.format(thisDay));
$day.text(thisDay.getDate());
if (thisDay.getMonth() != date.getMonth()) {
$day.addClass('overlap');
};
if (this.isUnavailable(thisDay)) {
$day.addClass('disabled');
};
if (this.$tentativesAsAvailable && this.isTentative(thisDay)) {
$day.addClass('tentative');
};
this.$days.append($day);
};
this.updateName(this.$month, this.monthNames[date.getMonth()]);
this.updateName(this.$year, this.curMonth.getFullYear());
this.$days.find('div:not(.disabled)').click($.proxy(function(e) {
var $targ = $(e.target);
// The date= attribute is used here to provide relatively fast
// selectors for setting certain date cells.
this.update($targ.attr("date"));
this.$days.find('div').removeClass('selected');
$targ.addClass('selected');
// Don't consider this selection final if we're just going to an
// adjacent month.
if(!$targ.hasClass('overlap')) {
this.hide();
}
}, this));
$("[date='" + this.format(this.$today) + "']", this.$days).addClass('today');
};
$('.selected', this.$days).removeClass('selected');
$('[date="' + this.selectedDateStr + '"]', this.$days).addClass('selected');
}
, isUnavailable: function(day) {
var dayIndex;
var weekDay = day.getDay();
if (typeof(this.$min) !== "undefined" && this.$min >= day) {
return true;
}
if (typeof(this.$availabilityStartDate) !== "undefined" &&
typeof(this.$availabilityMap) !== "undefined") {
dayIndex = this.daysBetween(this.$availabilityStartDate, day);
if (dayIndex >= 0 && this.$tentativesAsAvailable &&
this.$availabilityMap[dayIndex] !== "0" && this.$availabilityMap[dayIndex] !== "2") {
return true
} else if (dayIndex >= 0 && !this.$tentativesAsAvailable &&
this.$availabilityMap[dayIndex] !== "0") {
return true;
}
}
if (typeof(this.$blockedDays) !== "undefined" && this.$blockedDays[weekDay] !== '0') {
return true;
}
false
}
, isTentative: function(day) {
var dayIndex = this.daysBetween(this.$availabilityStartDate, day);
return this.$availabilityMap[dayIndex] === "2";
}
, selectDate: function(date) {
if (typeof(date) == "undefined") {
date = this.parse(this.$el.data('value') || this.$el.val());
};
if (!date) date = new Date();
this.selectedDate = date;
this.selectedDateStr = this.format(this.selectedDate);
this.selectMonth(this.selectedDate);
}
, update: function(s) {
this.$el.val(I18n.l("date.formats.input", s));
this.$el.data('value', s);
this.$el.attr('data-value', s);
this.$elApiInput.val(s);
this.$el.trigger('change');
}
, show: function(e) {
e && e.stopPropagation();
// Hide all other datepickers.
if (!STANDALONE) clearDatePickers(this);
var position = this.$el.position(); // Use position() instead of offset()
// Adjust left position if outside window
if ($(window).width() < this.$el.offset().left + this.$picker.outerWidth())
var left = position.left - this.$picker.outerWidth() + this.$el.outerWidth();
else
var left = position.left;
if (!STANDALONE) {
this.$picker.css({
top: position.top + this.$el.outerHeight(),
left: left,
position: 'absolute',
zIndex: '900',
margin: '0 0 18px 0'
});
}
this.$picker.show();
if (!STANDALONE) $('html').on('keydown', this.keyHandler);
}
, hide: function() {
if (!STANDALONE) this.$picker.hide();
if (!STANDALONE) $('html').off('keydown', this.keyHandler);
}
, keyHandler: function(e) {
// Keyboard navigation shortcuts.
switch (e.keyCode)
{
case 9:
case 27:
// Tab or escape hides the datepicker. In this case, just return
// instead of breaking, so that the e doesn't get stopped.
this.hide(); return;
case 13:
// Enter selects the currently highlighted date.
this.update(this.selectedDateStr); this.hide(); break;
case 38:
// Arrow up goes to prev week.
this.ahead(0, -7); break;
case 40:
// Arrow down goes to next week.
this.ahead(0, 7); break;
case 37:
// Arrow left goes to prev day.
this.ahead(0, -1); break;
case 39:
// Arrow right goes to next day.
this.ahead(0, 1); break;
default:
return;
}
e.preventDefault();
}
, parse: function(s) {
// Parse a partial RFC 3339 string into a Date.
var m;
if ((m = s.match(/^(\d{4,4})-(\d{2,2})-(\d{2,2})$/))) {
return new Date(m[1], m[2] - 1, m[3]);
} else {
return null;
}
}
, format: function(date) {
return I18n.strftime(date, '%Y-%m-%d')
}
, ahead: function(months, days) {
var newDay = new Date(this.selectedDate.getFullYear(),
this.selectedDate.getMonth() + months,
this.selectedDate.getDate() + days);
// Do not allow to move to invalid date
if (typeof(this.$min) !== "undefined" && this.$min > newDay) {
newDay = this.$min;
}
// Move ahead ``months`` months and ``days`` days, both integers, can be
// negative.
this.selectDate(newDay);
}
, proxy: function(meth) {
// Bind a method so that it always gets the datepicker instance for
// ``this``. Return ``this`` so chaining calls works.
this[meth] = $.proxy(this[meth], this);
return this;
}
, daysBetween: function(start, end) {
// Return number of days between ``start`` Date object and ``end``.
var start = Date.UTC(start.getFullYear(), start.getMonth(), start.getDate());
var end = Date.UTC(end.getFullYear(), end.getMonth(), end.getDate());
return (end - start) / 86400000;
}
, findClosest: function(dow, date, direction) {
// From a starting date, find the first day ahead of behind it that is
// a given day of the week.
var difference = direction * (Math.abs(date.getDay() - dow - (direction * 7)) % 7);
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + difference);
}
, rangeStart: function(date) {
// Get the first day to show in the current calendar view.
return this.findClosest(this.startOfWeek,
new Date(date.getFullYear(), date.getMonth()),
-1);
}
, rangeEnd: function(date) {
// Get the last day to show in the current calendar view.
return this.findClosest((this.startOfWeek - 1) % 7,
new Date(date.getFullYear(), date.getMonth() + 1, 0),
1);
}
};
/* DATEPICKER PLUGIN DEFINITION
* ============================ */
$.fn.datepicker = function( options ) {
return this.each(function() { new DatePicker(this, options); });
};
$(function() {
$(selector).datepicker();
if (!STANDALONE) {
$('html').click(clearDatePickers);
}
});
$.fn.datepicker.DatePicker = DatePicker;
$.fn.datepicker.defaults = {
monthNames: ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
, shortDayNames: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
, startOfWeek: 1
};
}( window.jQuery || window.ender || window.Zepto);
$background: #fff;
$nav-hover: #444;
$hover: green;
$selected: blue;
$today: #fee9cc;
$overlap: #ccc;
$tentative: #ccc;
.datepicker {
background-color: $background;
border-color: #999;
border-color: rgba(0, 0, 0, 0.2);
border-style: solid;
border-width: 1px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,.2);
// @include background-clip(padding-box);
display: none;
position: absolute;
padding-bottom: 4px;
width: 218px;
font-size: 12px;
color: #222;
z-index: 9999;
.nav {
font-weight: bold;
width: 100%;
padding: 4px 0;
background-color: #f5f5f5;
color: $gray;
border-bottom: 1px solid #ddd;
box-shadow: inset 0 1px 0 $background;
@include clearfix();
span {
display: block;
float: left;
text-align: center;
height: 28px;
line-height: 28px;
position: relative;
}
.bg {
width: 100%;
background-color: #fdf5d9;
height: 28px;
position: absolute;
top: 0;
left: 0;
border-radius: 4px;
}
.fg {
width: 100%;
position: absolute;
top: 0;
left: 0;
}
}
.button {
cursor: pointer;
padding: 0 4px;
border-radius: 4px;
&:hover {
background-color: $nav-hover;
color: $background;
}
}
.months {
float: left;
margin-left: 4px;
.name {
width: 72px;
padding: 0;
}
}
.years {
float: right;
margin-right: 4px;
.name {
width: 36px;
padding: 0;
}
}
.dow, .days div {
float: left;
width: 30px;
line-height: 3em;
text-align: center;
width: 14%;
}
.dow {
font-weight: bold;
color: $nav-hover;
}
.calendar {
padding: 4px;
}
.days {
clear: left;
div {
cursor: pointer;
border-radius: 4px;
&:hover {
background-color: $hover;
color: $background;
}
&.disabled:hover {
background-color: transparent;
color: $overlap;
}
}
}
.overlap,
.disabled {
color: $overlap;
}
.tentative:not(.disabled):not(.selected) {
@include linear-gradient(45deg, transparent, transparent 85%, $tentative 85%, $tentative);
&:hover {
@include linear-gradient(45deg, $hover, $hover 85%, $tentative 85%, $tentative);
}
}
.tentative.selected {
@include linear-gradient(45deg, $selected, $selected 85%, $tentative 85%, $tentative);
}
.today {
background-color: $today;
}
.selected {
background-color: $selected;
color: $background;
}
.legend {
border-top: 1px solid $tentative;
float: none;
clear: both;
.legend-item-wrapper {
margin-top: 5px;
display: block;
margin: 4px 0 0 0;
.legend-item {
display: inline-block;
width: 20px;
height: 20px;
border: 1px solid $tentative;
vertical-align: middle;
margin-left: 9px;
margin-right: 5px;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment