/** * * 888 d8b * 888 Y8P * 888 * .d8888b 8888b. 888 .d88b. 888d888 8888b. 88888b. 8888 .d8888b * d88P" "88b 888 d8P Y8b 888P" "88b 888 "88b "888 88K * 888 .d888888 888 88888888 888 .d888888 888 888 888 "Y8888b. * Y88b. 888 888 888 Y8b. 888 888 888 888 888 d8b 888 X88 * "Y8888P "Y888888 888 "Y8888 888 "Y888888 888 888 Y8P 888 88888P' * 888 * d88P * 888P" * * @name: caleran.js - the multiple date range picker * @description: An inline/popup date range picker with multiple selected range support, now with plain JS * @version: 1.0.2 * @author: Taha PAKSU * * * Change Log: * =========== * * v1.0.0 * ------ * - released first version * * v1.0.1 * ------ * - Fix swipe month switch direction * - Fix mobile calendar height limit calculation * - Fix input focus on dropdown open * - Add better swipe detection * * v1.0.2 * ------ * - Fixed swiping issues * * * * Usage: * ------ * caleran(".selector", {options}); */ ; (function (window, document, undefined) { 'use strict'; /** * The main caleran class * @class caleran */ var caleranPlugin = function (elem, options) { this.elem = elem; this.options = options; this.metadata = this.elem.getAttribute("data-plugin-options") ? JSON.parse(this.elem.getAttribute("data-plugin-options")) : {}; }; /** * Prototype of caleran plugin * @prototype caleran */ caleranPlugin.prototype = { ///////////////////////////////////////////////////////////////////// // public properties that can be set through plugin initialization // ///////////////////////////////////////////////////////////////////// public: function () { return { startDate: moment().startOf('day'), // (d) the selected start date, initially today endDate: moment().startOf('day'), // (d) the selected end date, initially today format: "L", // (d) the default format for showing in input, default short date format of locale dateSeparator: " - ", // (d) if not used as a single date picker, this will be the seperator calendarCount: 2, // (d) how many calendars will be shown in the plugin screen mobileBreakpoint: 760, // width in pixels to switch to mobile layout on initialization isHotelBooking: false, // (d) hotel booking style ranges inline: false, // (d) display as an inline input replacement minDate: null, // (d) minimum selectable date, default null (no minimum) maxDate: null, // (d) maximum selectable date, default null (no maximum) showHeader: true, // (d) visibility of the part which displays the selected start and end dates showFooter: true, // (d) visibility of the part which contains user defined ranges rangeOrientation: "horizontal", // (d) show ranges horizontally below the calendars or vertically on the side verticalRangeWidth: 180, // (d) range panel width when rangeOrientation is set to vertical showButtons: false, // (d) visibility of the part which contains the buttons on desktop dropdown view startOnMonday: false, // (d) if you want to start the calendars on monday, set this to true container: "body", // (d) the container of the dropdowns oneCalendarWidth: 230, // (d) the width of one calendar, if two calendars are shown, the input width will be 2 * this setting. enableKeyboard: true, // (d) enables keyboard navigation showOn: "bottom", // (d) dropdown placement position relative to input element ( "top", "left", "right", "bottom", "center") arrowOn: "left", // (d) arrow position ("left" "right" "center" for showOn "bottom" or "top", and vice versa) autoAlign: true, // (d) automatically reposition the picker when window resize or scroll or dropdown show locale: moment.locale(), // (d) moment locale setting, different inputs: https://momentjs.com/docs/#/i18n/changing-locale/ , available locales: https://momentjs.com/ (bottom of the page) singleDate: false, // (d) if you want a single date picker, set this to true target: null, // (d) the element to update after selection, defaults to the element that is instantiated on autoCloseOnSelect: false, // (d) closes the dropdown/modal after valid selection startEmpty: false, // (d) starts with no value selected isRTL: false, // flag to use RTL layout ranges: [{ // (d) default range objects array, one range is defined like title: "Today", // {title(string), startDate(moment object), endDate(moment object) } startDate: moment(), endDate: moment() }, { title: "3 Days", startDate: moment(), endDate: moment().add(2, "days") }, { title: "5 Days", startDate: moment(), endDate: moment().add(4, "days") }, { title: "1 Week", startDate: moment(), endDate: moment().add(6, "days") }, { title: "Till Next Week", startDate: moment(), endDate: moment().endOf("week") // if you use Monday as week start, you should use "isoweek" instead of "week" }, { title: "Till Next Month", startDate: moment(), endDate: moment().endOf("month") }], rangeLabel: "Ranges: ", // (d) the title of defined ranges cancelLabel: "Cancel", // (d) the text on the cancel button applyLabel: "Apply", // (d) the text on the apply button nextMonthIcon: "", // (d) the html of the next month icon prevMonthIcon: "", // (d) the html of the prev month icon rangeIcon: "", // (d) the html of the range label icon headerSeparator: "", // (d) the html of the icon between header dates onbeforeselect: function () { // (d) triggered before selection is applied, can be reverted with returning false return true; }, // (d) event which is triggered before selecting the end date ( a range selection is completed ) onafterselect: function () { }, // (d) event which is triggered after selecting the end date ( the input value changed ) onbeforeshow: function () { }, // (d) event which is triggered before showing the dropdown onbeforehide: function () { }, // (d) event which is triggered before hiding the dropdown onaftershow: function () { }, // (d) event which is triggered after showing the dropdown onafterhide: function () { }, // (d) event which is triggered after hiding the dropdown onfirstselect: function () { }, // (d) event which is triggered after selecting the first date of ranges onrangeselect: function () { }, // (d) event which is triggered after selecting a range from the defined range links onbeforemonthchange: function () { // (d) event which is triggered before switching the month, can be prevented with returning false return true; }, // (d) event which fires before changing the first calendar month of multiple calendars, or the month of a single calendar onaftermonthchange: function () { }, // (d) event which fires after changing the first calendar month of multiple calendars, or the month of a single calendar onafteryearchange: function () { }, // (d)event which fires after changing the first calendar year of multiple calendars, or the year of a single calendar ondraw: function () { }, // (d) event which fires after a complete redraw occurs onBeforeInit: function () { }, // (d) event which is fired before initialization and after configuration onBeforeDestroy: function () { return true; }, // (d) event which is fired before destruction oninit: function () { }, // (d) event which is fired after complete initialization ondestroy: function () { }, // (d) event which is fired after complete destruction validateClick: function () { return true; }, // (d) event which is fired on cell click, returning false will prevent selection onCancel: function(){ return true; }, // (d) event which is fired on cancel button click, returning false will prevent cancel disableDays: function () { // (d) function which is used to disable the related day with returning true after checks return false; }, disabledRanges: [], // (d) array which contains disabled date ranges, refer to docs for the structure continuous: false, // (d) flag to make sure the range selected doesn't contain disabled dates disableOnlyStart: false, // (d) flag to apply the disables only to start dates disableOnlyEnd: false, // (d) flag to apply the disables only to end days minSelectedDays: 0, // (d) minimum number of selected days to be accepted enableMonthSwitcher: true, // (d) flag to enable the month switcher when clicking the month name on the calendar titles enableYearSwitcher: true, // (d) flag to enable the year switcher when clicking the year text on the calendar titles enableSwipe: true, // (d) flag to enable the swiped month switch on mobile displays numericMonthSwitcher: false, // (d) flag to enable displaying numbers instead of month names in month switcher monthSwitcherFormat: "MMMM", // (d) Changes the month name display format on the month switcher. Default: "MMMM" showWeekNumbers: false, // (d) flag to show/hide week numbers hideOutOfRange: false, // (d) hides or disables out of range months, years, navigation arrows DOBCalendar: false // (d) Birthdate selection mode }; }, ////////////////////////////////////////// // private variables for internal usage // ////////////////////////////////////////// private: function () { return { startSelected: false, // flag which indicates the start date is selected on the calendar, and the next click will define the end date. currentDate: moment().startOf('day'), // the current month which is shown on the first calendar endSelected: true, // flag which indicates that the end date is selected. Also means that a complete range is selected. hoverDate: null, // the day element which is currently being hovered via mouse keyboardHoverDate: null, // the day element which is currently being hovered via keyboard headerStartDay: null, // header start day information container element headerStartDate: null, // header start date of month container element headerStartWeekday: null, // header start week day text container element headerEndDay: null, // header end day information container element headerEndDate: null, // header end date of month container element headerEndWeekday: null, // header end week day text container element swipeX: 0, swipeY: 0, swipeStart: 0, swipeTimeout: null, // swipe debouncing timeout variable isMobile: false, // current environment is mobile or desktop indicator valElements: ["BUTTON", "OPTION", "INPUT", "LI", "METER", "PROGRESS", "PARAM"], // elements which support value property dontHideOnce: false, // flag that tells the dropdown not to close once initiator: null, // element which triggered the dropdown to show initComplete: false, // is the plugin completely initialized? startDateBackup: null, // start date clone while switching months, used for range selection after month/year switch startDateInitial: null, // start date clone when displaying the plugin to use on cancellation operation endDateInitial: null, // end date clone when displaying the plugin to use on cancellation operation firstValueSelected: false, // used with config.startEmpty, set to true if the initial range selection has been made. throttleTimeout: null, // debouncer function timeout variable instanceId: null, documentEvent: null, // used for separating document bound events for multiple instances delayInputUpdate: false, // used for preventing the input to be updated until the apply button is clicked lastScrollX: 0, // horizontal buffer variable of scroll positions for using inside requestAnimationFrame lastScrollY: 0, // vertical buffer variable of scroll positions for using inside requestAnimationFrame isTicking: false, // requestAnimationFrame ticking suppressor parentScrollEventsAttached: false, // runonce flag for input's scrollable parents, which scroll events are attached once rafID: 0, // requestanimationframe ID for cancellation disabledDays: {} }; }, /** * initialize the plugin * @return caleran object */ init: function () { this.config = this.extend(this.extend(this.public(), this.options), this.metadata); this.globals = this.private(); this.globals.isMobile = this.checkMobile(); this.globals.instanceId = Math.round((+new Date()) + (Math.random() * 100)); this.events = this.eventsPrototype(); this.events.init(this.globals.instanceId); this.config.onBeforeInit(this); this.applyConfig(); this.fetchInputs(); this.drawUserInterface(); this.drawOverlay(); this.addInitialEvents(); this.addKeyboardEvents(); this.elem.caleran = this; this.config.oninit(this); this.globals.initComplete = true; this.globals.lastScrollX = 0; this.globals.lastScrollY = 0; this.events.dispatch(window, "resize"); return this; }, /** * validates start and end dates, * swaps dates if end > start, * sets visible month of first selection * * @return void */ validateDates: function () { // validate start & end dates var swap; if ( ( (moment.isMoment(this.config.startDate) && this.config.startDate.isValid()) || (moment(this.config.startDate, this.config.format).isValid()) ) && ( (moment.isMoment(this.config.endDate) && this.config.endDate.isValid()) || (moment(this.config.endDate, this.config.format).isValid()) ) ) { this.config.startDate = moment(this.config.startDate, this.config.format).middleOfDay().locale(this.config.locale); this.config.endDate = moment(this.config.endDate, this.config.format).middleOfDay().locale(this.config.locale); if (this.config.startDate.isAfter(this.config.endDate, "day")) { swap = this.config.startDate.clone(); this.config.startDate = this.config.endDate.clone(); this.config.endDate = swap.clone(); swap = null; } } else { this.config.startDate = moment().middleOfDay().locale(this.config.locale); this.config.endDate = moment().middleOfDay().locale(this.config.locale); } this.globals.currentDate = moment(this.config.startDate, this.config.format); // validate min & max dates if (this.config.minDate !== null && moment(this.config.minDate, this.config.format).isValid()) { this.config.minDate = moment(this.config.minDate, this.config.format).middleOfDay(); } else { this.config.minDate = null; } if (this.config.maxDate !== null && moment(this.config.maxDate, this.config.format).isValid()) { this.config.maxDate = moment(this.config.maxDate, this.config.format).middleOfDay(); } else { this.config.maxDate = null; } if (this.config.minDate !== null && this.config.maxDate !== null && this.config.minDate.isAfter(this.config.maxDate, "day")) { swap = this.config.minDate.clone(); this.config.minDate = this.config.maxDate.clone(); this.config.maxDate = swap.clone(); swap = null; } // validate start and end dates according to min and max dates if (this.config.minDate !== null && this.config.startDate !== null && this.config.minDate.isAfter(this.config.startDate, "day")) { this.config.startDate = this.config.minDate.clone(); } if (this.config.minDate !== null && this.config.endDate !== null && this.config.minDate.isAfter(this.config.endDate, "day")) { this.config.endDate = this.config.minDate.clone(); } if (this.config.maxDate !== null && this.config.startDate !== null && this.config.maxDate.isBefore(this.config.startDate, "day")) { this.config.startDate = this.config.maxDate.clone(); } if (this.config.maxDate !== null && this.config.endDate !== null && this.config.maxDate.isBefore(this.config.endDate, "day")) { this.config.endDate = this.config.maxDate.clone(); } if (this.checkRangeContinuity() === false || (this.config.disableOnlyStart == true && this.isDisabled(this.config.startDate, this.config.isHotelBooking)) || (this.config.singleDate === false && this.config.disableOnlyEnd && this.isDisabled(this.config.endDate, this.config.isHotelBooking)) || (this.config.startEmpty && this.globals.firstValueSelected == false) ) { this.clearInput(); } }, /** * sets config variables and relations between them, * for example "inline" property converts the input to hidden input, * applies default date from input to plugin and vice versa .. etc. * * @return void */ applyConfig: function () { // set target element to be updated if (this.config.target === null) this.config.target = this.elem; // disable dobcalendar if inline if (this.config.inline === true) this.config.DOBCalendar = false; // switch to singledate when dobcalendar is true if (this.config.DOBCalendar == true) this.config.singleDate = true; if(typeof this.config.container === "string") this.config.container = document.querySelector(this.config.container); // fix dates var props = ["startDate", "endDate", "minDate", "maxDate"]; for(var p = 0; p < props.length; p++){ var key = props[p]; this.config[key] = this.fixDateTime(this.config[key]); } this.config.ranges.map( function(range) { range.startDate = this.fixDateTime(range.startDate); range.endDate = this.fixDateTime(range.endDate); return range; }, this); // create container relative to environment and settings if (this.globals.isMobile === false) { if (this.config.inline === true) { this.container = document.createElement("div"); this.container.classList.add("caleran-container", "caleran-inline"); this.container.tabIndex = 1; this.container.onclick = ''; this.elem.insertAdjacentElement("beforebegin", this.container); this.container.appendChild(this.elem); this.input = document.createElement("div"); this.input.classList.add("caleran-input"); this.container.appendChild(this.input); this.elem.type = "hidden"; this.config.showButtons = false; this.setViewport(); } else { this.container = document.createElement("div"); this.container.classList.add("caleran-container", "caleran-popup"); this.container.style.display = 'none'; this.container.setAttribute("onclick", ''); var arrow = document.createElement("div"); arrow.classList.add("caleran-box-arrow-top"); this.container.appendChild(arrow); this.input = document.createElement("div"); this.input.classList.add("caleran-input"); this.container.appendChild(this.input); this.config.container.appendChild(this.container); if (this.config.showButtons) { this.globals.delayInputUpdate = true; this.config.autoCloseOnSelect = false; } } if (this.config.rangeOrientation === "horizontal") { this.input.style.width = (this.config.calendarCount * this.config.oneCalendarWidth) + "px"; } else { this.input.style.width = ((this.config.calendarCount * this.config.oneCalendarWidth) + this.config.verticalRangeWidth) + "px"; } } else { if (this.config.inline === true) { this.container = document.createElement("div"); this.container.classList.add("caleran-container-mobile", "caleran-inline"); this.container.tabIndex = 1; this.container.onclick = ''; this.elem.parentNode.insertBefore(this.container, this.elem); this.container.appendChild(this.elem); this.input = document.createElement("div"); this.input.classList.add("caleran-input"); this.container.appendChild(this.input); this.elem.type = "hidden"; this.config.showButtons = false; } else { this.container = document.createElement("div"); this.container.classList.add(["caleran-container-mobile"]); this.container.onclick = ''; this.config.container.appendChild(this.container); this.input = document.createElement("div"); this.input.classList.add("caleran-input"); this.input.style.display = 'none'; this.container.appendChild(this.input); if (this.config.showButtons) { this.config.autoCloseOnSelect = false; } if (!this.config.autoCloseOnSelect) this.globals.delayInputUpdate = true; } // disable the soft keyboard on mobile devices this.elem.onfocus = function(){ this.blur(); }; } if (this.config.isHotelBooking) { this.container.classList.add("caleran-hotel-style"); } if(this.config.isRTL) { this.container.style.direction = "rtl"; this.container.classList.add("caleran-rtl"); } this.clearRangeSelection(); }, /** * Clears the selected range info */ clearRangeSelection: function () { for (var range = 0; range < this.config.ranges.length; range++) { this.config.ranges[range].selected = false; } var ranges = this.container.querySelectorAll(".caleran-range"); for(var r = 0; r < ranges.length; r++){ ranges[r].classList.remove("caleran-range-selected"); } }, inArray: function(needle, haystack){ return Array.isArray(haystack) && haystack.indexOf(needle) >= 0; }, /** * Parse input from the source element's value and apply to config * @return void */ fetchInputs: function () { var elValue = this.inArray(this.config.target.tagName, this.globals.valElements) ? this.config.target.value : this.config.target.innerText; if (this.config.singleDate === false && elValue.indexOf(this.config.dateSeparator) > 0) { var parts = elValue.split(this.config.dateSeparator); if (parts.length == 2) { if (moment(parts[0], this.config.format, this.config.locale).isValid() && moment(parts[1], this.config.format, this.config.locale).isValid()) { this.config.startDate = moment(parts[0], this.config.format, this.config.locale).middleOfDay(); this.config.endDate = moment(parts[1], this.config.format, this.config.locale).middleOfDay(); this.globals.firstValueSelected = true; } } } else if (this.config.singleDate === true) { var value = elValue; if (value != "" && moment(value, this.config.format, this.config.locale).isValid()) { this.config.startDate = moment(value, this.config.format, this.config.locale).middleOfDay(); this.config.endDate = moment(value, this.config.format, this.config.locale).middleOfDay(); this.globals.firstValueSelected = true; } }// clear input if startEmpty is defined if (this.config.startEmpty && !this.globals.firstValueSelected) { this.clearInput(); } // validate inputs this.validateDates(); }, /** * Draws the plugin interface when needed * @return void */ drawUserInterface: function () { this.drawHeader(); this.calendars = this.input.querySelectorAll(".caleran-calendars")[0]; var nextCal = this.globals.currentDate.clone().middleOfDay(); this.globals.disabledDays = {}; for (var calendarIndex = 0; calendarIndex < this.config.calendarCount; calendarIndex++) { this.drawCalendarOfMonth(nextCal); nextCal = nextCal.add(1, "month"); } // remove last border var calendars = this.calendars.querySelectorAll(".caleran-calendar"); calendars[calendars.length - 1].classList.add("no-border-right"); this.drawArrows(); this.drawFooter(); if ((this.globals.isMobile === true || this.config.inline === false) && this.globals.initComplete) { this.setViewport(); } if (this.globals.startSelected === false) { if (this.globals.initComplete) { this.updateInput(false); } else { var delayState = this.globals.delayInputUpdate; this.globals.delayInputUpdate = false; this.updateInput(false); this.globals.delayInputUpdate = delayState; } } this.reDrawCells(); }, /** * Draws the overlay to prevent clickthroughs when an instance is open on mobile view * @return void */ drawOverlay: function () { if (this.globals.isMobile == false) return; if (document.querySelectorAll(".caleran-overlay").length == 0) { this.overlay = document.createElement("div"); this.overlay.classList.add("caleran-overlay"); document.body.appendChild(this.overlay); var overlayEvents = function(){ var visibleInstances = document.querySelectorAll("body > .caleran-container-mobile"); if(visibleInstances.length > 0){ for(var vi = 0; vi < visibleInstances.length; vi++){ var item = visibleInstances[vi]; if(item.style.display != "none"){ item.querySelector(".caleran-cancel").click(); } } } }; this.events.add(this.overlay, "click", overlayEvents, false); this.events.add(this.overlay, "tap", overlayEvents, false); } else { this.overlay = document.querySelectorAll(".caleran-overlay")[0]; } }, /** * Draws the header part of the plugin, which contains start and end date displays * @return void */ drawHeader: function () { var headers = "
" + "
" + "
" + "
" + "
" + "
"; if (this.config.singleDate === false) { headers += "
" + this.config.headerSeparator + "
" + "
" + "
" + "
" + "
" + "
"; } headers += "
"; this.input.insertAdjacentHTML("beforeend", headers); if (this.config.showHeader === false) { headers = this.input.querySelectorAll(".caleran-header"); for(var h = 0; h < headers.length; h++) { headers[h].style.display = 'none'; } } this.globals.headerStartDay = this.input.querySelector(".caleran-header-start-day"); this.globals.headerStartDate = this.input.querySelector(".caleran-header-start-date"); this.globals.headerStartWeekday = this.input.querySelector(".caleran-header-start-weekday"); this.globals.headerEndDay = this.input.querySelector(".caleran-header-end-day"); this.globals.headerEndDate = this.input.querySelector(".caleran-header-end-date"); this.globals.headerEndWeekday = this.input.querySelector(".caleran-header-end-weekday"); this.updateHeader(); }, /** * Updates the start and end dates in the header * @return void */ updateHeader: function () { if (this.config.startDate) this.config.startDate.locale(this.config.locale); if (this.config.endDate) this.config.endDate.locale(this.config.locale); if (this.config.startEmpty && this.globals.firstValueSelected === false) return; if (this.config.startDate !== null) { this.globals.headerStartDay.innerText = this.localizeNumbers(this.config.startDate.date()); if (this.globals.isMobile) this.globals.headerStartDate.innerText = this.config.startDate.format("MMM") + " " + this.localizeNumbers(this.config.startDate.year()); else this.globals.headerStartDate.innerText = this.config.startDate.format("MMMM") + " " + this.localizeNumbers(this.config.startDate.year()); this.globals.headerStartWeekday.innerText = this.config.startDate.format("dddd"); } else { this.globals.headerStartDay.innerText = ""; this.globals.headerStartDate.innerText = ""; this.globals.headerStartWeekday.innerText = ""; } if (this.config.singleDate === false) { if (this.config.endDate !== null) { this.globals.headerEndDay.innerText = this.localizeNumbers(this.config.endDate.date()); if (this.globals.isMobile) this.globals.headerEndDate.innerText = this.config.endDate.format("MMM") + " " + this.localizeNumbers(this.config.endDate.year()); else this.globals.headerEndDate.innerText = this.config.endDate.format("MMMM") + " " + this.localizeNumbers(this.config.endDate.year()); this.globals.headerEndWeekday.innerText = this.config.endDate.format("dddd"); } else { this.globals.headerEndDay.innerText = ""; this.globals.headerEndDate.innerText = ""; this.globals.headerEndWeekday.innerText = ""; } } }, /** * checks for updateInput to be run or dismissed * @return {boolean} whether the input should be updated or not */ isUpdateable: function () { var returnReasons = this.globals.delayInputUpdate; var clearReasons = this.config.startEmpty && !this.globals.firstValueSelected; clearReasons = clearReasons || (this.config.singleDate === true && this.config.startDate === null); clearReasons = clearReasons || (this.config.singleDate === false && (this.config.startDate === null || this.config.endDate === null)); if (clearReasons) this.clearInput(); if (clearReasons || returnReasons) return false; return true; }, /** * Updates the connected input element value when the value is chosen * @return void */ updateInput: function (withEvents) { if (this.config.startDate) this.config.startDate.locale(this.config.locale); if (this.config.endDate) this.config.endDate.locale(this.config.locale); if (!this.isUpdateable()) return; if (this.inArray(this.config.target.tagName, this.globals.valElements)) { if (this.config.singleDate === false) { this.config.target.value = this.config.startDate.format(this.config.format) + this.config.dateSeparator + this.config.endDate.format(this.config.format); } else { this.config.target.value = this.config.startDate.format(this.config.format); } } else { if (this.config.singleDate === false) { this.config.target.innerText = this.config.startDate.format(this.config.format) + this.config.dateSeparator + this.config.endDate.format(this.config.format); } else { this.config.target.innerText = this.config.startDate.format(this.config.format); } } if (this.globals.initComplete === true && withEvents === true) { this.config.onafterselect(this, this.config.startDate.clone(), this.config.endDate.clone()); this.events.dispatch(this.input, "change"); } }, /** * Clears the input and prepares it for a new date range selection * @return void */ clearInput: function (stayEmpty) { if (this.inArray(this.config.target.tagName, this.globals.valElements)) { if (this.config.singleDate === false) this.config.target.value = ""; else this.config.target.value = ""; } else { if (this.config.singleDate === false) this.config.target.innerText = ""; else this.config.target.innrtText = ""; } this.config.startDate = null; this.config.endDate = null; if(stayEmpty) { this.config.startEmpty = true; this.globals.firstValueSelected = false; } else { if(this.config.startEmpty == true) this.globals.firstValueSelected = false; } if (this.globals.initComplete) { this.updateHeader(); var applyButton = typeof this.footer == "undefined" ? [] : this.footer.querySelector(".caleran-apply"); if (applyButton.length > 0) applyButton.setAttribute("disabled", "disabled"); } }, /** * Draws the arrows of the month switcher * @return void */ drawArrows: function () { var hideLeftArrow = this.config.hideOutOfRange && this.config.minDate && this.globals.currentDate.clone().add(-1, "month").isBefore(this.config.minDate, "month"); var hideRightArrow = this.config.hideOutOfRange && this.config.maxDate && this.globals.currentDate.clone().add(this.config.calendarCount, "month").isAfter(this.config.maxDate, "month"); var titles = this.container.querySelectorAll(".caleran-title"); if (titles.length > 0) { var t, item; if (this.globals.isMobile) { if (!hideLeftArrow) { for(t = 0; t < titles.length; t++){ titles[t].insertAdjacentHTML("afterbegin", "
" + this.config.prevMonthIcon + "
"); } } if (!hideRightArrow) { for(t = 0; t < titles.length; t++){ titles[t].insertAdjacentHTML("beforeend", "
" + this.config.nextMonthIcon + "
"); } } } else { if(this.config.isRTL){ if (!hideLeftArrow) titles[titles.length - 1].insertAdjacentHTML("afterbegin", "
" + this.config.prevMonthIcon + "
"); if (!hideRightArrow) titles[0].insertAdjacentHTML("beforeend", "
" + this.config.nextMonthIcon + "
"); }else{ if (!hideLeftArrow) titles[0].insertAdjacentHTML("afterbegin", "
" + this.config.prevMonthIcon + "
"); if (!hideRightArrow) titles[titles.length - 1].insertAdjacentHTML("beforeend", "
" + this.config.nextMonthIcon + "
"); } } } }, /** * Draws a single calendar * @param [momentobject] _month: The moment object containing the desired month, for example given "18/02/2017" as moment object draws the calendar of February 2017. * @return void */ drawCalendarOfMonth: function (_month) { var calendarStart = moment(_month).locale(this.config.locale).startOf("month").startOf("isoweek").middleOfDay(); var startOfWeek = calendarStart.day(); if (startOfWeek == 1 && this.config.startOnMonday === false) { calendarStart.add(-1, "days"); startOfWeek = 0; } else if (startOfWeek === 0 && this.config.startOnMonday === true) { calendarStart.add(1, "days"); startOfWeek = 1; } if (calendarStart.isAfter(moment(_month).date(1))) calendarStart.add(-7, "day"); var calendarOutput = "
"; var boxCount = 0; var monthClass = "", yearClass = ""; if (this.config.enableMonthSwitcher) monthClass = " class='caleran-month-switch'"; if (this.config.enableYearSwitcher) yearClass = " class='caleran-year-switch'"; calendarOutput += "
" + _month.locale(this.config.locale).format("MMMM") + " " + this.localizeNumbers(_month.year()) + "
"; calendarOutput += "
"; var localeWeekdays = moment.localeData(this.config.locale).weekdaysShort(); if (this.config.showWeekNumbers) calendarOutput += "
 
"; for (var days = startOfWeek; days < startOfWeek + 7; days++) { calendarOutput += "
" + localeWeekdays[days % 7] + "
"; } var prevDisabled = true, prevKey = null; while (boxCount < 42) { var cellDate = calendarStart.middleOfDay().unix(); var cellStyle = (_month.month() == calendarStart.month()) ? "caleran-day" : "caleran-disabled"; if (boxCount % 7 === 0 && this.config.showWeekNumbers) { calendarOutput += "
" + calendarStart.format("ww") + "
"; } calendarOutput += "
" + this.localizeNumbers(calendarStart.date()) + "
"; /** * Disabled Date Values * 1 => Start of Range disabled * 2 => Fully disabled * 3 => End of Range disabled */ if (this.isDisabledOnDraw(calendarStart)) { this.globals.disabledDays[cellDate] = prevDisabled == true ? 2 : 1; prevDisabled = true; prevKey = cellDate; } else { if (prevDisabled == true && prevKey != null) { this.globals.disabledDays[prevKey] = 3; } prevDisabled = false; } calendarStart.add(moment.duration({ "days": 1 })); boxCount++; } calendarOutput += "
"; calendarOutput += "
"; this.calendars.insertAdjacentHTML("beforeend", calendarOutput); }, /** * Draws the footer of the plugin, which contains range selector links * @return void */ drawFooter: function () { if (this.config.singleDate === false && this.config.showFooter === true) { if (this.config.rangeOrientation === "horizontal" || this.globals.isMobile) { this.input.insertAdjacentHTML("beforeend", "
"); } else { this.input.classList.add("caleran-input-vertical-range"); var innerWrap = document.createElement("div"); innerWrap.classList.add("caleran-left"); while(this.input.firstChild) innerWrap.appendChild(this.input.firstChild); this.input.appendChild(innerWrap); innerWrap.insertAdjacentHTML("afterend", "
"); } var ranges = this.input.querySelector(".caleran-ranges"); ranges.insertAdjacentHTML("beforeend", "" + this.config.rangeIcon + "
" + this.config.rangeLabel + "
"); for (var range_id = 0; range_id < this.config.ranges.length; range_id++) { ranges.insertAdjacentHTML("beforeend", "
" + this.config.ranges[range_id].title + "
"); } } if (this.globals.isMobile && !this.config.inline) { if (this.config.singleDate === true || this.config.showFooter === false) { this.input.insertAdjacentHTML("beforeend", "
"); } } if ((this.globals.isMobile && !this.config.inline) || (!this.globals.isMobile && !this.config.inline && this.config.showButtons)) { if (this.config.rangeOrientation === "horizontal" || this.globals.isMobile) { this.input.insertAdjacentHTML("beforeend", ""); } else { this.input.querySelector(".caleran-right").insertAdjacentHTML("beforeend", ""); } this.footer = this.input.querySelector(".caleran-footer"); this.footer.insertAdjacentHTML("beforeend", ""); this.footer.insertAdjacentHTML("beforeend", ""); if (this.globals.firstValueSelected === false && this.config.startEmpty == true) { this.footer.querySelector(".caleran-apply").setAttribute("disabled", "disabled"); } if (this.globals.isMobile && this.globals.endSelected === false) { this.footer.querySelector(".caleran-apply").setAttribute("disabled", "disabled"); } } }, /** * Draws next month's calendar (just calls this.reDrawCalendars with an 1 month incremented currentDate) * Used in the next arrow click event * * @return void */ drawNextMonth: function (event) { event = event || window.event; if(this.config.hideOutOfRange == true && this.config.maxDate && this.globals.currentDate.clone().add(this.config.calendarCount, "month").isAfter(this.config.maxDate, "month")) { return false; } if (this.globals.swipeTimeout === null) { var that = this; this.globals.swipeTimeout = setTimeout(function () { if (that.config.onbeforemonthchange(that, that.globals.currentDate.clone().startOfMonth(), "next") === true) { var buffer = that.calendars.scrollTop; that.globals.currentDate.middleOfDay().add(1, "month"); that.reDrawCalendars(); that.calendars.scrollTop = buffer; that.config.onaftermonthchange(that, that.globals.currentDate.clone().startOfMonth()); } that.globals.swipeTimeout = null; }, 100); } if(event.isCancellable) return this.stopBubbling(event); return false; }, /** * Draws previous month's calendar (just calls this.reDrawCalendars with an 1 month decremented currentDate) * Used in the prev arrow click event * * @return void */ drawPrevMonth: function (event) { event = event || window.event; if(this.config.hideOutOfRange == true && this.config.minDate && this.globals.currentDate.clone().add(-1, "month").isBefore(this.config.minDate, "month")) { return false; } if (this.globals.swipeTimeout === null) { var that = this; this.globals.swipeTimeout = setTimeout(function () { if (that.config.onbeforemonthchange(that, that.globals.currentDate.clone().startOfMonth(), "prev") === true) { var buffer = that.calendars.scrollTop; that.globals.currentDate.middleOfDay().subtract(1, "month"); that.reDrawCalendars(); that.calendars.scrollTop = buffer; that.config.onaftermonthchange(that, that.globals.currentDate.clone().startOfMonth()); } that.globals.swipeTimeout = null; }, 100); } if(event.isCancellable) return this.stopBubbling(event); return false; }, /** * Day cell click event handler * @param [eventobject] e : The event object which contains the clicked cell in e.target property, which enables us to select dates * @return void */ cellClicked: function (e) { var event = e || window.event; var target = event.target || event.srcElement; if (target.classList.contains("caleran-day") === false) target = this.closest(target, ".caleran-day"); var cell = parseInt(target.getAttribute("data-value"), 10); var selectedMoment = moment.unix(cell).middleOfDay(); if (this.config.validateClick(selectedMoment) == false) return false; if (this.config.singleDate === false) { if (this.globals.startSelected === false) { if (this.config.startDate !== null) this.globals.startDateBackup = this.config.startDate.clone(); this.config.startDate = selectedMoment; this.config.endDate = null; this.globals.startSelected = true; this.globals.endSelected = false; var applyButton = typeof this.footer == "undefined" ? [] : this.footer.querySelector(".caleran-apply"); if (applyButton.length > 0) applyButton.setAttribute("disabled", "disabled"); this.config.onfirstselect(this, this.config.startDate.clone()); } else { if (selectedMoment.isBefore(this.config.startDate)) { var start = this.config.startDate.clone(); this.config.startDate = selectedMoment.clone(); selectedMoment = start; } if (selectedMoment.diff(this.config.startDate, "day") < this.config.minSelectedDays) { this.globals.startSelected = false; this.fetchInputs(); } else { this.globals.startDateBackup = null; this.config.endDate = selectedMoment; this.globals.endSelected = true; this.globals.startSelected = false; this.globals.hoverDate = null; if (this.config.onbeforeselect(this, this.config.startDate.clone(), this.config.endDate.clone()) === true && this.checkRangeContinuity() === true) { this.globals.firstValueSelected = true; this.clearRangeSelection(); this.updateInput(true); } else this.fetchInputs(); if (this.config.autoCloseOnSelect && (this.config.inline === false)) { this.hideDropdown(event); } else { if (typeof this.footer != "undefined" && this.config.endDate != null) { this.footer.querySelector(".caleran-apply").removeAttribute("disabled"); } } } } } else { this.config.startDate = selectedMoment; this.config.endDate = selectedMoment; this.globals.endSelected = true; this.globals.startSelected = false; this.globals.hoverDate = null; if (this.config.onbeforeselect(this, this.config.startDate.clone(), this.config.endDate.clone()) === true) { this.globals.firstValueSelected = true; this.clearRangeSelection(); this.updateInput(true); } else { this.fetchInputs(); } if (this.config.autoCloseOnSelect && (this.config.inline === false)) { this.hideDropdown(event); } else { if (typeof this.footer != "undefined" && this.config.endDate != null) { this.footer.querySelector(".caleran-apply").removeAttribute("disabled"); } } } this.reDrawCells(); this.updateHeader(); return this.stopBubbling(event); }, /** * Checks if the defined range is continous (doesn't include disabled ranges or disabled days by callback) * @return boolean is continuous or not */ checkRangeContinuity: function () { var daysInRange = this.config.endDate.diff(this.config.startDate, "days"); var startDate = moment(this.config.startDate).middleOfDay(); /** * variables affecting this: * ------------------------- * config.isHotelBooking * config.continuous * config.disableOnlyStart * config.disableOnlyEnd * * if disableOnlyStart or disableOnlyEnd is active, skip continuousity. * else check continuousity * */ if (this.config.disableOnlyStart == true) { // if disabling only start is active, just start date check will be sufficient // false means continuousity is preserved, true means the date is disabled return this.isDisabled(this.config.startDate, this.config.isHotelBooking) === false; } else if (this.config.disableOnlyEnd == true) { // if disabling only end is active, just end date check will be sufficient // false means continuousity is preserved, true means the date is disabled return this.isDisabled(this.config.endDate, this.config.isHotelBooking) === false; } else { if(this.config.continuous) { var startDateUnix = startDate.middleOfDay().unix(); //check if startdate can be a start date if ( // hotel style active, get only disabled. // hotel style passing, get disabled. this.isDisabled(startDateUnix, false) == true && // if hotel style is active check if start date is a range's start date (this.config.isHotelBooking ? this.getDisabledLevel(startDateUnix) === 1 : true)) { return false; } if(startDate.isSame(this.config.endDate, "day") == false){ startDate.middleOfDay().add(1, "days"); for (var i = 0; i <= daysInRange - 2; i++) { startDateUnix = startDate.middleOfDay().unix(); if (this.getDisabledLevel(startDateUnix) !== undefined) return false; startDate.add(1, "days"); } } // check if enddate can be an end date startDateUnix = startDate.middleOfDay().unix(); if ( // hotel style active, get only disabled. // hotel style passing, get disabled. this.isDisabled(startDateUnix, false) == true && // if hotel style is active check if end date is a range's end date (this.config.isHotelBooking ? this.getDisabledLevel(startDateUnix) === 3 : true)) { return false; } } } return true; }, /** * Checks if given moment value is disabled for that calendar on first draw * @param [moment] day : The day to be checked * @return {boolean} If the day is disabled or not */ isDisabledOnDraw: function (day) { var mday = moment(day).middleOfDay(); if (this.config.disableDays(mday) === true) { return true; }; for (var rangeIndex = 0; rangeIndex < this.config.disabledRanges.length; rangeIndex++) { var range = this.config.disabledRanges[rangeIndex]; if (mday.isBetween(range.start, range.end, "day", "[]")) { return true; } } }, /** * Checks if given moment value is disabled for that calendar from the disabled array * @param [moment] day : The day to be checked * @return {boolean} If the day is disabled or not */ isDisabled: function (day, hotelStyle) { if (undefined === hotelStyle) hotelStyle = false; if (this.config.disableOnlyStart == true && this.globals.startSelected == true) { return false; } else if (this.config.disableOnlyEnd == true && this.globals.startSelected == false) { return false; } else { if (typeof day == "object" && day !== null) { day = day.middleOfDay().unix(); } if (hotelStyle && this.config.isHotelBooking) { return this.globals.disabledDays[day] === 2; } return this.globals.disabledDays[day] !== undefined; } }, /** * Gets the disabledDays record for the given day * @param integer|object day */ getDisabledLevel: function (day) { if (typeof day == "object" && day !== null) { day = day.middleOfDay().unix(); } return this.globals.disabledDays[day]; }, /** * Event triggered when a day is hovered * @param [eventobject] e : The event object which contains the hovered cell in e.target property, which enables us to style hovered dates * @return void */ cellHovered: function (e) { var event = e || window.event; var target = event.target || event.srcElement; if (target.classList.contains("caleran-day") === false) target = this.closest(target, ".caleran-day"); if(target) { var cell = parseInt(target.getAttribute("data-value"), 10); this.globals.hoverDate = moment.unix(cell).middleOfDay(); this.globals.keyboardHoverDate = null; if (this.globals.startSelected === true) this.reDrawCells(); return this.stopBubbling(event); } }, /** * Just a calendar refresher to be used with the new variables, updating the calendar view * @return void */ reDrawCalendars: function () { //this.requestAnimFrame(this.proxy(function(){ this.input.innerHTML = ""; this.drawUserInterface(); this.container.focus(); /*if (this.globals.lastScrollY !== 0) { window.scrollTo(this.globals.lastScrollX, this.globals.lastScrollY); }*/ //}, this)); }, /** * month switcher builder and processor method * @return void */ monthSwitchClicked: function () { if (this.calendars.querySelectorAll(".caleran-month-selector").length > 0) return; var that = this; this.calendars.scrollTop = 0; this.calendars.insertAdjacentHTML("beforeend", "
"); var monthSelector = this.calendars.querySelector(".caleran-month-selector"); var currentMonth = this.globals.currentDate.get('month'); for (var m = 0; m < 12; m++) { monthSelector.insertAdjacentHTML("beforeend", "
" + (this.config.numericMonthSelector ? (m + 1) : moment({ day: 15, hour: 12, month: m }).locale(this.config.locale).format(this.config.monthSwitcherFormat)) + "
"); } monthSelector.style.display = "block"; this.optimizeFontSize(monthSelector.querySelectorAll(".caleran-ms-month")); this.rebindEventEach(monthSelector, ".caleran-ms-month", "click", function (event) { if(!that.globals) return false; that.globals.currentDate.month(parseInt(event.target.getAttribute("data-month"), 10)); that.config.onaftermonthchange(that, that.globals.currentDate.clone().startOfMonth()); that.reDrawCalendars(); return that.stopBubbling(event); }); }, /** * year switcher builder and processor method * @return void */ yearSwitchClicked: function () { if (this.calendars.querySelectorAll(".caleran-year-selector").length > 0) return; var that = this; this.calendars.scrollTop = 0; this.calendars.insertAdjacentHTML("beforeend", "
"); var yearSelector = this.calendars.querySelector('.caleran-year-selector'); var currentYear = this.globals.currentDate.get('year'); yearSelector.insertAdjacentHTML("beforeend", "
"); yearSelector.setAttribute("data-year", currentYear); for (var year = currentYear - 6; year < currentYear + 7; year++) { yearSelector.insertAdjacentHTML("beforeend", "
" + this.localizeNumbers(year) + "
"); } yearSelector.insertAdjacentHTML("beforeend", "
"); yearSelector.style.display = "block"; this.optimizeFontSize(yearSelector.querySelectorAll(".caleran-ys-year")); this.rebindEventScoped(document, ".caleran-ys-year", "click.caleranys", function (event) { if(!that.globals) return false; that.globals.currentDate.year(parseInt(event.target.getAttribute("data-year"))); that.config.onafteryearchange(that, that.globals.currentDate.clone().startOf("year")); that.reDrawCalendars(); if(that.config.DOBCalendar == true){ that.calendars.querySelectorAll(".caleran-calendar")[0].querySelector(".caleran-month-switch").click(); } return that.stopBubbling(event); }); this.rebindEventScoped(document, ".caleran-ys-year-prev", "click.caleranysprev", function (event) { if(!that.globals) return false; var currentYear = parseInt(yearSelector.getAttribute("data-year"), 10) - 13; var currentYearNow = that.globals.currentDate.get('year'); yearSelector.setAttribute("data-year", currentYear); yearSelector.innerHTML = ""; yearSelector.insertAdjacentHTML("beforeend", "
"); for (var year = currentYear - 6; year < currentYear + 7; year++) { yearSelector.insertAdjacentHTML("beforeend", "
" + that.localizeNumbers(year) + "
"); } yearSelector.insertAdjacentHTML("beforeend", "
"); return that.stopBubbling(event); }); this.rebindEventScoped(document, ".caleran-ys-year-next", "click.caleranysnext", function (event) { if(!that.globals) return false; var currentYear = parseInt(yearSelector.getAttribute("data-year"), 10) + 13; var currentYearNow = that.globals.currentDate.get('year'); yearSelector.setAttribute("data-year", currentYear); yearSelector.innerHTML = ""; yearSelector.insertAdjacentHTML("beforeend", "
"); for (var year = currentYear - 6; year < currentYear + 7; year++) { yearSelector.insertAdjacentHTML("beforeend", "
" + that.localizeNumbers(year) + "
"); } yearSelector.insertAdjacentHTML("beforeend", "
"); return that.stopBubbling(event); }); }, /** * Lowers the font size until all the text fits in the element */ optimizeFontSize: function (element) { for(var i = 0; i < element.length; i++){ var elem = element[i]; var wrapper = document.createElement("span"), parent = elem; wrapper.classList.add("adjust-subject"); while(elem.firstChild) wrapper.appendChild(elem.firstChild); parent.appendChild(wrapper); parent.insertAdjacentHTML("beforeend", "i"); var adjustSubject = wrapper; var fontAdjuster = parent.querySelector(".font-adjuster"); if (adjustSubject.offsetHeight === fontAdjuster.offsetHeight) { fontAdjuster.parentNode.removeChild(fontAdjuster); while(adjustSubject.firstChild) parent.appendChild(adjustSubject.firstChild); adjustSubject.parentNode.removeChild(adjustSubject); } else { var loopCount = 0; while (adjustSubject.offsetHeight !== fontAdjuster.offsetHeight && loopCount < 16) { var startSize = 0; if (typeof window.getComputedStyle !== "undefined") { startSize = parseFloat(window.getComputedStyle(fontAdjuster, null).getPropertyValue('font-size')); } else { startSize = parseFloat(fontAdjuster.style.fontSize.replace(/px$/, "")); } adjustSubject.parentNode.style.fontSize = (startSize - 1) + "px"; fontAdjuster.style.fontSize = (startSize - 1) + "px"; if (startSize < 2) break; loopCount++; } fontAdjuster.parentNode.removeChild(fontAdjuster); while(adjustSubject.firstChild) parent.appendChild(adjustSubject.firstChild); adjustSubject.parentNode.removeChild(adjustSubject); } } }, /** * Shows the caleran dropdown * @return void */ showDropdown: function (e) { var event = e || window.event || jQuery.Event("click", { target: this.elem }); var eventTarget = event.target || event.srcElement; if ((!this.globals.isMobile && this.container.style.display == "none") || (this.globals.isMobile && this.input.style.display == "none")) { if (eventTarget !== this.elem) { this.globals.dontHideOnce = true; this.globals.initiator = eventTarget; } this.fetchInputs(); this.reDrawCalendars(); this.globals.startDateInitial = this.config.startDate; this.globals.endDateInitial = this.config.endDate; this.config.onbeforeshow(this); if (this.globals.isMobile) { this.input.style.display = "flex"; this.overlay.style.display = "block"; document.body.classList.add("caleran-open"); } else { this.container.style.display = "block"; } this.setViewport(); if(this.config.DOBCalendar == true){ this.calendars.querySelectorAll(".caleran-calendar")[0].querySelector(".caleran-year-switch").click(); } this.config.onaftershow(this); } }, /** * Hides the caleran dropdown * @return void */ hideDropdown: function (e) { var event = e || window.event || new Event("click", { target: "body" }); var eventTarget = event.target || event.srcElement; if (this.globals.initiator === eventTarget) return; if (this.config.inline === false && ((!this.globals.isMobile && this.container.style.display !== "none") || (this.globals.isMobile && this.input.style.display !== "none"))) { this.config.onbeforehide(this); if (this.globals.isMobile) { this.input.style.display = "none"; this.overlay.style.display = "none"; document.body.classList.remove("caleran-open"); } else { this.container.style.display = "none"; } this.globals.hoverDate = null; if (this.globals.startDateBackup !== null) { this.config.startDate = this.globals.startDateBackup; this.globals.startSelected = false; } this.config.onafterhide(this); } }, /** * When only a cell style update is needed, this function is used. This gives us the possibility to change styles without re-drawing the calendars. * @return void */ reDrawCells: function () { var that = this; var startDateUnix = this.config.startDate != null ? this.config.startDate.middleOfDay().unix() : null; var endDateUnix = this.config.endDate != null ? this.config.endDate.middleOfDay().unix() : null; var minDateUnix = this.config.minDate != null ? this.config.minDate.middleOfDay().unix() : null; var maxDateUnix = this.config.maxDate != null ? this.config.maxDate.middleOfDay().unix() : null; var hoverDateUnix = this.globals.hoverDate != null ? this.globals.hoverDate.middleOfDay().unix() : null; var keyboardHoverDateUnix = this.globals.keyboardHoverDate != null ? this.globals.keyboardHoverDate.middleOfDay().unix() : null; var currentDateUnix = moment().middleOfDay().unix(); this.lastHoverStatus = false; for (var c = 0; c < this.config.calendarCount; c++) { var calendar = this.input.querySelectorAll(".caleran-calendar")[c]; var cells = calendar.querySelectorAll(".caleran-days-container > div:not(.caleran-dayofweek):not(.caleran-weeknumber)"); var currentMonth = parseInt(calendar.getAttribute("data-month"), 10); for (var i = 0; i < cells.length; i++) { var cell = cells[i]; var cellDate = parseInt(cell.getAttribute("data-value"), 10); var cellMoment = moment.unix(cellDate).middleOfDay().locale(that.config.locale); var cellStyle = "caleran-day"; var cellDay = cellMoment.day(); // is weekend day (saturday or sunday) check if (cellDay == 6 || cellDay === 0) cellStyle += " caleran-weekend"; // is today check if (cellDate === currentDateUnix) cellStyle += " caleran-today"; cellStyle = this.addDisabledStyles(cell, cellMoment, cellDate, cellStyle, minDateUnix, maxDateUnix, currentMonth); cellStyle = this.addSelectedStyles(cellDate, cellStyle, startDateUnix, endDateUnix, minDateUnix, maxDateUnix); cellStyle = this.addHoverStyles(cell, cellDate, cellStyle, this, startDateUnix, hoverDateUnix, keyboardHoverDateUnix); cell.setAttribute("class", cellStyle); } } this.attachEvents(); this.config.ondraw(this); }, /** * returns calculated selected state style of the cell * @param cellMoment current cell's day * @param cellStyle current cell's already calculated style * @return appended style of the cell */ addSelectedStyles: function (cellDateUnix, cellStyle, startDateUnix, endDateUnix, minDateUnix, maxDateUnix) { var that = this; var cellStyleOriginal = cellStyle; if (that.config.startEmpty === false || that.globals.firstValueSelected) { // is the start of the range check if (that.config.singleDate === false && startDateUnix !== null && startDateUnix === cellDateUnix) cellStyle += " caleran-start"; // is the end of the range check if (that.config.singleDate === false && endDateUnix !== null && endDateUnix === cellDateUnix) cellStyle += " caleran-end"; // is between the start and the end range check if (that.config.singleDate === false && startDateUnix !== null && endDateUnix !== null && cellDateUnix <= endDateUnix && cellDateUnix >= startDateUnix) cellStyle += " caleran-selected"; // is the selected date of single date picker if (that.config.singleDate === true && startDateUnix !== null && startDateUnix === cellDateUnix) cellStyle += " caleran-selected caleran-start caleran-end"; } //if (cellStyleOriginal != cellStyle) { cellStyle = cellStyle.replace("caleran-disabled", "caleran-day"); } return cellStyle; }, /** * returns calculated hovered state style of the cell * @param cellMoment current cell's day * @param cellStyle current cell's already calculated style * @return appended style of the cell */ addHoverStyles: function (cell, cellDateUnix, cellStyle, ref, startDateUnix, hoverDateUnix, keyboardHoverDateUnix) { // hovered date check var that = this; cellStyle.replace("caleran-hovered", "").replace("caleran-hovered-last", "").replace("caleran-hovered-first", ""); if (that.globals.startSelected === true && that.globals.endSelected === false && hoverDateUnix !== null) { if ((cellDateUnix >= hoverDateUnix && cellDateUnix <= startDateUnix) || (cellDateUnix <= hoverDateUnix && cellDateUnix >= startDateUnix)) { cellStyle += " caleran-hovered"; } } if (that.config.enableKeyboard == true && keyboardHoverDateUnix !== null) { if (that.globals.startSelected === false) { if (keyboardHoverDateUnix == cellDateUnix) { cellStyle += " caleran-hovered"; } } else { if ((cellDateUnix <= startDateUnix && cellDateUnix >= keyboardHoverDateUnix) || (cellDateUnix >= startDateUnix && cellDateUnix <= keyboardHoverDateUnix)) { cellStyle += " caleran-hovered"; } } } if (this.lastHoverStatus === false && cellStyle.indexOf("caleran-hovered") > 0) { this.lastHoverStatus = true; cellStyle += " caleran-hovered-first"; } if (this.lastHoverStatus === true && cellStyle.indexOf("caleran-hovered") < 0) { var prev = cell.previousElementSibling; prev = prev && prev.matches('.caleran-day') ? prev : null; if(prev) prev.classList.add("caleran-hovered-last"); this.lastHoverStatus = false; } return cellStyle; }, /** * returns calculated disabled state style of the cell * @param {object} cell current cell jquery object * @param {object} cellMoment current cell's moment * @param {integer} cellDateUnix current call's moment unix timestamp * @param {string} cellStyle current cell's already calculated style * @param {integer} minDateUnix minDate config moment unix timestamp * @param {integer} maxDateUnix maxDate config moment unix timestamp * @returns {string} appended style of the cell */ addDisabledStyles: function (cell, cellMoment, cellDateUnix, cellStyle, minDateUnix, maxDateUnix, currentMonth) { if (this.isDisabled(cellDateUnix)) { if (this.config.isHotelBooking == false) { cellStyle = cellStyle.replace("caleran-day", "caleran-disabled caleran-disabled-range"); } else { switch (this.globals.disabledDays[cellDateUnix]) { case 1: cellStyle = cellStyle.replace("caleran-day", "caleran-day caleran-disabled-range caleran-disabled-range-start"); break; case 2: cellStyle = cellStyle.replace("caleran-day", "caleran-disabled caleran-disabled-range"); break; case 3: cellStyle = cellStyle.replace("caleran-day", "caleran-day caleran-disabled-range caleran-disabled-range-end"); break; } } } else if ((maxDateUnix != null && cellDateUnix > maxDateUnix) || (minDateUnix != null && cellDateUnix < minDateUnix)) { cellStyle = cellStyle = cellStyle.replace("caleran-day", "caleran-disabled"); } if (cellMoment.month() != currentMonth) { cellStyle += " caleran-not-in-month"; } return cellStyle; }, /** * Localizes the given number to the current locale * @param {string} input The number to be localized * @returns {string} The localized number string */ localizeNumbers: function (input) { return moment.localeData(this.config.locale).postformat('' + input); }, /** * Event triggered when a range link is clicked in the footer of the plugin * @param {object} e the clicked range link * @returns void */ rangeClicked: function (e) { var event = e || window.event; var target = event.target || event.srcElement; if (!target.hasAttribute("data-id")) return; var range_id = target.getAttribute("data-id"); this.globals.currentDate = this.config.ranges[range_id].startDate.startOf('day').clone().middleOfDay(); this.config.startDate = this.config.ranges[range_id].startDate.startOf('day').clone().middleOfDay(); this.config.endDate = this.config.ranges[range_id].endDate.startOf('day').clone().middleOfDay(); this.globals.firstValueSelected = true; if (this.checkRangeContinuity() === false) { this.fetchInputs(); } else { this.clearRangeSelection(); this.config.ranges[range_id].selected = true; this.config.onrangeselect(this, this.config.ranges[range_id]); this.reDrawCalendars(); this.setViewport(); if (this.config.autoCloseOnSelect) this.hideDropdown(); } return this.stopBubbling(event); }, /** * Fixes the view positions of dropdown calendar plugin * @returns void */ setViewport: function () { if(this.globals) { if (this.globals.isMobile === true) { if (this.input.style.display !== "none") { this.events.dispatch(this.container, "caleran:resize"); } } else { if (this.container.style.display !== "none" && this.globals.initComplete && (this.globals.isMobile === false && this.config.inline === false)) { var viewport = this.getViewport(); var result = false; switch (this.config.showOn) { case "top": result = (this.config.autoAlign) ? this.positionOnTopAlign(viewport) : this.positionOnTop(false, viewport); result = this.horizontalAlign(viewport); break; case "left": result = (this.config.autoAlign) ? this.positionOnLeftAlign(viewport) : this.positionOnLeft(false, viewport); result = this.verticalAlign(viewport); break; case "right": result = (this.config.autoAlign) ? this.positionOnRightAlign(viewport) : this.positionOnRight(false, viewport); result = this.verticalAlign(viewport); break; case "bottom": result = (this.config.autoAlign) ? this.positionOnBottomAlign(viewport) : this.positionOnBottom(false, viewport); result = this.horizontalAlign(viewport); break; case "center": result = this.positionOnCenter(viewport); break; default: result = (this.config.autoAlign) ? this.positionOnBottomAlign(viewport) : this.positionOnBottom(false, viewport); result = this.horizontalAlign(viewport); break; } if (this.config.rangeOrientation !== "horizontal") { var height = this.input.querySelector(".caleran-header").clientHeight + this.input.querySelector(".caleran-calendars").clientHeight + ((this.input.querySelectorAll(".caleran-footer").length > 0) ? this.input.querySelector(".caleran-footer").clientHeight : 0); this.input.querySelector(".caleran-right").style.maxHeight = height + "px"; } } } } }, /** * Vertically aligns the dropdown when the viewport changes * @param {object} viewport An object containing the current viewport dimensions */ verticalAlign: function(viewport) { var dropdown = this.getDimensions(this.container, true), start_difference = viewport.top - dropdown.offsetTop, end_difference = dropdown.offsetTop + dropdown.height - viewport.bottom, arrow = this.container.querySelector("div[class*='caleran-box-arrow-']"); if(start_difference > 0 && Math.abs(start_difference) < dropdown.height) { this.container.style.top = (parseFloat(this.container.style.top.replace(/px$/, "")) + start_difference) + "px"; arrow.style.top = (parseFloat(arrow.style.top.replace(/px$/, "")) - start_difference) + "px"; } else if(end_difference > 0 && Math.abs(end_difference) < dropdown.height){ this.container.style.top = (parseFloat(this.container.style.top.replace(/px$/, "")) - end_difference) + "px"; arrow.style.top = (parseFloat(arrow.style.top.replace(/px$/, "")) + end_difference) + "px"; } }, /** * Horizontally aligns the dropdown when the viewport changes * @param {object} viewport An object containing the current viewport dimensions */ horizontalAlign: function(viewport) { var dropdown = this.getDimensions(this.container, true), difference = dropdown.offsetLeft + dropdown.width - viewport.right, arrow = this.container.querySelector("div[class*='caleran-box-arrow-']"); if(difference > 0 && Math.abs(difference) < dropdown.width){ this.container.style.left = (parseFloat(this.container.style.left.replace(/px$/,"")) - difference) + "px"; arrow.style.left = parseFloat(arrow.style.left.replace(/px$/,"")) + difference; } }, /** * Gets the coordinates of the dropdown relative to the position * @param string position */ getDropdownPos: function (position) { var input = this.getDimensions(this.elem, true); // console.log(input); // this.debugPosition("inputpos", input.offsetTop, input.offsetLeft, input.offsetLeft + input.width, input.offsetTop + input.height, "rgba(0,0,128,0.5)"); var dropdown = this.getDimensions(this.container, true); var margin = parseInt(this.input.style.marginLeft, 10); if(isNaN(margin)) margin = 0; var arrow = 0; if(this.container.querySelectorAll("div[class*='caleran-box-arrow']").length > 0) { arrow = parseFloat(this.container.querySelectorAll("div[class*='caleran-box-arrow']")[0].offsetHeight / 2); } switch (position) { case "left": switch (this.config.arrowOn) { case "top": return { top: input.offsetTop - margin - arrow - (input.height / 2), left: input.offsetLeft - dropdown.width - margin, arrow: 0 }; case "center": return { top: input.offsetTop - margin - (dropdown.height / 2), left: input.offsetLeft - dropdown.width - margin, arrow: (dropdown.height - (arrow * 2)) / 2 - (input.height / 2) }; case "bottom": return { top: input.offsetTop - dropdown.height + input.height + (2 * margin) + arrow, left: input.offsetLeft - dropdown.width - margin, arrow: dropdown.height - (arrow * 4) - (3 * margin) - (input.height / 2) }; default: return { top: input.offsetTop - margin - arrow - (input.height / 2), left: input.offsetLeft - dropdown.width - margin, arrow: 0 }; } break; case "right": switch (this.config.arrowOn) { case "top": return { top: input.offsetTop - margin - arrow - (input.height / 2), left: input.offsetLeft + input.width + margin, arrow: 0 }; case "center": return { top: input.offsetTop - margin - (dropdown.height / 2), left: input.offsetLeft + input.width + margin, arrow: (dropdown.height - (arrow * 2)) / 2 - (input.height / 2) }; case "bottom": return { top: input.offsetTop - dropdown.height + input.height + (2 * margin) + arrow, left: input.offsetLeft + input.width + margin, arrow: dropdown.height - (arrow * 4) - (3 * margin) - (input.height / 2) }; default: return { top: input.offsetTop - margin - arrow - (input.height / 2), left: input.offsetLeft + input.width + margin, arrow: 0 }; } break; case "top": switch (this.config.arrowOn) { case "left": return { top: input.offsetTop - dropdown.height - margin, left: input.offsetLeft - margin, arrow: 0 }; case "center": return { top: input.offsetTop - dropdown.height - margin, left: input.offsetLeft - ((dropdown.width - (margin * 2) - input.width) / 2), arrow: (dropdown.width - (arrow * 5)) / 2 }; case "right": return { top: input.offsetTop - dropdown.height - margin, left: input.offsetLeft - (dropdown.width - input.width) + margin, arrow: dropdown.width - (5 * arrow) + margin }; default: return { top: input.offsetTop - dropdown.height - margin, left: input.offsetLeft - margin, arrow: 0 }; } break; case "bottom": switch (this.config.arrowOn) { case "left": return { top: input.offsetTop + input.height - margin + arrow, left: input.offsetLeft - margin, arrow: 0 }; case "center": return { top: input.offsetTop + input.height - margin + arrow, left: input.offsetLeft - ((dropdown.width - (margin * 2) - input.width) / 2), arrow: (dropdown.width - (arrow * 5)) / 2 }; case "right": return { top: input.offsetTop + input.height - margin + arrow, left: input.offsetLeft - (dropdown.width - input.width) + margin, arrow: dropdown.width - (5 * arrow) + margin }; default: return { top: input.offsetTop + input.height - margin + arrow, left: input.offsetLeft - margin, arrow: 0 }; } break; case "center": switch (this.config.arrowOn) { case "center": return { top: input.offsetTop - margin - (dropdown.height / 2), left: input.offsetLeft - ((dropdown.width - (margin * 2) - input.width) / 2) }; } break; } }, /** * Moves the plugin dropdown relative to the input or return the calculated areas * @param {boolean} returnValues whether the method should apply the CSS or return the calculated values * @param {object} viewport the current viewport boundaries, top, right, bottom, left in pixels * @returns {object} if returnValues is set to true, it returns the calculated positions */ positionOnTop: function (returnValues, viewport) { var setting = this.getDropdownPos("top"); if (!returnValues) { this.container.style.left = (setting.left - 5) + "px"; this.container.style.top = (setting.top - 5) + "px"; var arrow = this.container.querySelector("div[class*='caleran-box-arrow-']"); arrow.setAttribute("class", "caleran-box-arrow-bottom"); arrow.style.left = setting.arrow + "px"; } else { return setting; } }, /** * Moves the plugin dropdown relative to the input or return the calculated areas * @param {boolean} returnValues whether the method should apply the CSS or return the calculated values * @param {object} viewport the current viewport boundaries, top, right, bottom, left in pixels * @returns {object} if returnValues is set to true, it returns the calculated positions */ positionOnBottom: function (returnValues, viewport) { var setting = this.getDropdownPos("bottom"); if (!returnValues) { this.container.style.left = (setting.left - 5) + "px"; this.container.style.top = (setting.top - 5) + "px"; var arrow = this.container.querySelector("div[class*='caleran-box-arrow-']"); arrow.setAttribute("class", "caleran-box-arrow-top"); arrow.style.left = setting.arrow + "px"; } else { return setting; } }, /** * Moves the plugin dropdown relative to the input or return the calculated areas * @param {boolean} returnValues whether the method should apply the CSS or return the calculated values * @param {object} viewport the current viewport boundaries, top, right, bottom, left in pixels * @returns {object} if returnValues is set to true, it returns the calculated positions */ positionOnLeft: function (returnValues, viewport) { var setting = this.getDropdownPos("left"); if (!returnValues) { this.container.style.left = (setting.left - 5) + "px"; this.container.style.top = (setting.top - 5) + "px"; var arrow = this.container.querySelector("div[class*='caleran-box-arrow-']"); arrow.setAttribute("class", "caleran-box-arrow-right"); arrow.style.top = setting.arrow + "px"; } else { return setting; } }, /** * Moves the plugin dropdown relative to the input or return the calculated areas * @param {boolean} returnValues whether the method should apply the CSS or return the calculated values * @param {object} viewport the current viewport boundaries, top, right, bottom, left in pixels * @returns {object} if returnValues is set to true, it returns the calculated positions */ positionOnRight: function (returnValues, viewport) { var setting = this.getDropdownPos("right"); if (!returnValues) { this.container.style.left = (setting.left + 5) + "px"; this.container.style.top = (setting.top - 5) + "px"; var arrow = this.container.querySelector("div[class*='caleran-box-arrow-']"); arrow.setAttribute("class", "caleran-box-arrow-left"); arrow.style.top = setting.arrow + "px"; } else { return setting; } }, /** * Moves the plugin dropdown relative to the input * @param {object} viewport the current viewport boundaries, top, right, bottom, left in pixels * @returns {object} */ positionOnCenter: function (viewport) { var setting = this.getDropdownPos("center"); var offsetX = Math.max((setting.left + this.container.clientWidth) - (viewport.right - 30), 0); var offsetY = Math.max((setting.top + this.container.clientHeight) - (viewport.bottom - 30), 0); if (!this.config.autoAlign) { offsetX = 0; offsetY = 0; } setting.left -= offsetX; setting.top -= offsetY; this.container.style.left = setting.left; this.container.style.top = setting.top; var arrow = this.container.querySelector("div[class*='caleran-box-arrow-']"); if(arrow) arrow.parentNode.removeChild(arrow); }, /** * Position the plugin dropdown relative to the input or return the calculated areas, and fixes if any overflow occurs */ positionOnBottomAlign: function (viewport) { var standardPosition = this.positionOnBottom(true, viewport); var dropdown = this.getDimensions(this.container); //this.debugPosition("bottomalign", standardPosition.top, standardPosition.left, standardPosition.left + dropdown.width, standardPosition.top + dropdown.height, "rgba(0,128,0,0.5)"); if (standardPosition.top + dropdown.height < viewport.bottom) { this.positionOnBottom(false, viewport); } else { this.positionOnTop(false, viewport); } }, /** * Position the plugin dropdown relative to the input or return the calculated areas, and fixes if any overflow occurs */ positionOnLeftAlign: function (viewport) { var standardPosition = this.positionOnLeft(true, viewport); if (standardPosition.left > viewport.left - 50) { this.positionOnLeft(false, viewport); } else { this.positionOnRight(false, viewport); } }, /** * Position the plugin dropdown relative to the input or return the calculated areas, and fixes if any overflow occurs */ positionOnRightAlign: function (viewport) { var standardPosition = this.positionOnRight(true, viewport); var dropdown = this.getDimensions(this.container); if (standardPosition.left + dropdown.width < viewport.right + 50) { this.positionOnRight(false, viewport); } else { this.positionOnLeft(false, viewport); } }, /** * Position the plugin dropdown relative to the input or return the calculated areas, and fixes if any overflow occurs */ positionOnTopAlign: function (viewport) { var standardPosition = this.positionOnTop(true, viewport); if (standardPosition.top > viewport.top) { this.positionOnTop(false, viewport); } else { this.positionOnBottom(false, viewport); } }, /** * Helper method for getting an element's inner/outer dimensions and positions * @param [DOMelement] elem The element whose dimensions and position are wanted * @param {boolean} outer true/false variable which tells the method to return inner(false) or outer(true) dimensions * @return {object} an user defined object which contains the width and height of the element and top and left positions relative to the viewport */ getDimensions: function (element, outer) { var off = { left: 0, top: 0 }, parent = element; this.attachScrollEvents(element); // now return the dimensions while(parent != null && parent != document.body) { var style = window.getComputedStyle(parent); if(style.position == "fixed") { off.top = off.top + window.scrollY + parseInt(style.top, 10); off.left = off.left + window.scrollX + parseInt(style.left, 10); break; } else { off.left = off.left + parent.offsetLeft; off.top = off.top + parent.offsetTop; parent = parent.offsetParent; } } return { width: element.offsetWidth, height: element.offsetHeight, offsetLeft: off.left, offsetTop: off.top }; }, attachScrollEvents: function(element) { var doc = document, body = doc.body; if (element === this.elem && this.globals.parentScrollEventsAttached == false) { // give each scroll parent a callback when they'll run on scrolling var scrollParent = this.proxy(function () { if(this.globals){ // dont run while it's already running if (!this.globals.isTicking) { this.globals.isTicking = true; // rAF technique implementation this.globals.rafID = this.requestAnimFrame(this.proxy(function () { this.setViewport(); this.cancelAnimFrame(this.globals.rafID); this.globals.isTicking = false; }, this)); } } }, this); // find the scrollable parents if (element !== body) { var parent = element.parentNode; while (parent !== body && parent !== null) { // if the parent is scrollable if (parent.scrollHeight > parent.offsetHeight) { // attach the scroll event this.rebindEvent(parent, "scroll", scrollParent); } // switch to next parent parent = parent.parentNode; } } // don't attach again once attached. this.globals.parentScrollEventsAttached = true; } }, /** * Helper method for getting the window viewport * @return {object} an user defined object which contains the rectangular position and dimensions of the viewport */ getViewport: function () { var top = this.globals.lastScrollY, left = this.globals.lastScrollX, bottom = top + window.innerHeight, right = left + window.innerWidth; //this.debugPosition("caleran-viewport", top + 30, left + 30, right - 30, bottom - 30, "rgba(128,0,0,0.5)"); return { top: top, left: left, right: right, bottom: bottom }; }, debugPosition: function(name, top, left, right, bottom, color){ if(document.body.querySelectorAll("#"+name).length > 0){ document.body.removeChild(document.body.querySelector("#"+name)); } document.body.insertAdjacentHTML("beforeend", "
;
"); var vpelem = document.querySelector("#"+name); vpelem.style.top = (top) + "px"; vpelem.style.left = (left) + "px"; vpelem.style.right = (right) + "px"; vpelem.style.bottom = (bottom) + "px"; vpelem.style.position = "absolute"; vpelem.style.backgroundColor = color; vpelem.style.zIndex = 100000; vpelem.style.opacity = 0.5; vpelem.style.width = (right - left) + "px"; vpelem.style.height = (bottom - top) + "px"; vpelem.style.pointerEvents = "none"; }, rebindEventEach: function(container, selector, event, callback, once){ var elems = container.querySelectorAll(selector); for(var el = 0; el < elems.length; el++) { this.rebindEvent(elems[el], event, callback, once); } }, rebindEvent: function(element, event, callback, once){ var events = event.split(" ").map(function(item) { return item.trim(); }); for(var e = 0, len = events.length; e < len; e++) { this.events.remove(element, events[e], callback); this.events.add(element, events[e], callback, once); } }, rebindEventScoped: function(element, scope, event, callback, once){ this.rebindEvent(element, event, function(e) { var eventTarget = e.target || e.srcElement; // loop parent nodes from the target to the delegation node if (eventTarget && (this.matches(eventTarget, scope) || this.closest(eventTarget, scope))) { return callback.call(eventTarget, e); } }.bind(this), once); }, matches: function(element, scope){ if (element && !element.matches) { var proto = Object.getPrototypeOf(element); proto.matches = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector || proto.oMatchesSelector || proto.webkitMatchesSelector; try { return proto.matches(scope); }catch(e){ return false; } } else { return element.matches(scope); } }, closest: function(element, selector){ var that = this; if (element && !element.closest) { var proto = Object.getPrototypeOf(element); proto.closest = function(s) { var el = this; do { if (that.matches(el, s)) return el; el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; }; try { return proto.closest(selector); }catch(e){ return false; } } else { return element.closest(selector); } }, removeEventEach: function(container, selector, event) { var events = event.split(" ").map(function(item) { return item.trim(); }), that = this; for(var e = 0, len = events.length; e < len; e++) { var items = container.querySelectorAll(selector); for(var i = 0, len2 = items.length; i < len2; i++){ this.events.remove(items[i], events[e]); } } }, /** * Attaches the related events on the elements after render/update * @return void */ attachEvents: function () { var clickNextEvent = this.drawNextMonth.bind(this); var clickPrevEvent = this.drawPrevMonth.bind(this); var clickCellEvent = this.cellClicked.bind(this); var hoverCellEvent = this.cellHovered.bind(this); var rangeClickedEvent = this.rangeClicked.bind(this); var monthSwitchClickEvent = this.monthSwitchClicked.bind(this); var yearSwitchClickEvent = this.yearSwitchClicked.bind(this); var clickEvent = "click"; this.rebindEventEach(this.container, ".caleran-next", clickEvent, !this.config.isRTL ? clickNextEvent : clickPrevEvent, true); this.rebindEventEach(this.container, ".caleran-prev", clickEvent, !this.config.isRTL ? clickPrevEvent : clickNextEvent, true); this.rebindEventEach(this.container, ".caleran-day", clickEvent, clickCellEvent, false); this.rebindEventEach(this.container, ".caleran-day", "mouseover", hoverCellEvent, false); this.removeEventEach(this.container, ".caleran-disabled:not(.caleran-day)", clickEvent); this.rebindEventEach(this.container, ".caleran-range", clickEvent, rangeClickedEvent, false); this.rebindEventEach(this.container, ".caleran-month-switch", clickEvent, monthSwitchClickEvent, false); this.rebindEventEach(this.container, ".caleran-year-switch", clickEvent, yearSwitchClickEvent, false); if (this.globals.isMobile === true && this.config.enableSwipe == true) { this.rebindEvent(this.container, "touchstart", function(e){ if(this.globals && event.changedTouches.length > 0){ this.globals.swipeX = event.changedTouches[0].screenX; this.globals.swipeY = event.changedTouches[0].screenY; this.globals.swipeStart = +new Date(); } }.bind(this)); this.rebindEvent(this.container, "touchend", function(event){ if(this.globals && event.changedTouches.length > 0 && Math.abs(this.globals.swipeY - event.changedTouches[0].screenY) < 50 && Math.abs(this.globals.swipeX - event.changedTouches[0].screenX) > 100 && ((+new Date()) - this.globals.swipeStart) < 250) { if(this.globals.swipeX < event.changedTouches[0].screenX) clickNextEvent(); else if(this.globals.swipeX >= event.changedTouches[0].screenX) clickPrevEvent(); } }.bind(this)); } if ((this.globals.isMobile || this.config.showButtons) && !this.config.inline) { this.rebindEventEach(this.input, ".caleran-cancel", "click", this.proxy(function (event) { if(this.config.onCancel(this, this.config.startDate, this.config.endDate) == true) { if (this.globals.startDateInitial) this.config.startDate = this.globals.startDateInitial.clone(); if (this.globals.endDateInitial) this.config.endDate = this.globals.endDateInitial.clone(); this.hideDropdown(event); } }, this), false); this.rebindEventEach(this.input, ".caleran-apply", "click", this.proxy(function (event) { if (this.config.onbeforeselect(this, this.config.startDate.clone(), this.config.endDate.clone()) === true && this.checkRangeContinuity() === true) { this.globals.firstValueSelected = true; if (this.globals.delayInputUpdate) { this.globals.delayInputUpdate = false; this.updateInput(true); this.globals.delayInputUpdate = true; } else { this.updateInput(true); } } else { this.fetchInputs(); } this.hideDropdown(event); }, this), false); } }, isVisible: function( elem ) { return elem.offsetWidth > 0 && elem.offsetHeight > 0; }, /** * Events per instance */ addInitialEvents: function () { // buffer current scope var that = this; // create namespaced event name var eventClick = "click"; // create a instance specific unique document click event, which will be used to hide the dropdown this.events.add(document, "click", this.proxy(function (e) { if (this.globals && this.globals.isMobile === false && this.config.inline === false) { var event = e || window.event || new Event("click", { target: "body" }); var eventTarget = event.target || event.srcElement; if (this.contains(this.container, eventTarget) === false && this.elem !== eventTarget && this.isVisible(this.container)) { this.hideDropdown(event); } } }, this)); // if the keyboard interaction is enabled, redefine the click event if (this.config.enableKeyboard) eventClick = "click focus"; // prepare the input click + focus event which will open the dropdown this.rebindEvent(this.elem, eventClick, this.proxy(this.debounce(function (e) { var event = e || window.event || new Event("click", { target: "body" }); var eventTarget = event.target || event.srcElement; if (this.input.clientHeight > 0 && this.config.target !== eventTarget) { this.hideDropdown(event); } else { this.events.dispatch(document, "click"); this.showDropdown(event); } }, 100, true), this), false); // on mobile screens, add an event to restyle the modal when a layout change occurs if (this.globals.isMobile) { this.events.add(window, "resize", function () { that.events.dispatch(that.container, "caleran:resize"); }); } // define the mobile resize event this.events.add(this.container, "caleran:resize", function () { that.globals.rafID = that.requestAnimFrame(function () { if(that.globals) { if (that.input.style.display !== "none") { var oneCalendarHeight = that.input.querySelector(".caleran-calendar:first-child"); if(oneCalendarHeight && that.isVisible(oneCalendarHeight)) { oneCalendarHeight = oneCalendarHeight.clientHeight; that.input.querySelector(".caleran-calendars").style.height = oneCalendarHeight + "px"; if (that.input.offsetTop < 0) that.input.classList.add("caleran-input-top-reset"); if (window.innerWidth > window.innerHeight) { // landscape mode that.input.style.height = oneCalendarHeight + "px"; } else { // portrait mode that.input.style.height = "auto"; } } that.cancelAnimFrame(that.globals.rafID); } } }); }); // run the event once if the environment is mobile and (possibly) display is inline if (this.input.style.display !== "none" && this.globals.isMobile) { this.events.dispatch(this.container, "caleran:resize"); } // append the desktop layout refreshing event if (this.globals.isMobile === false) { this.rebindEvent(window, "resize scroll", function () { // dont run while it's already running if(that.globals) { if (!that.globals.isTicking) { that.globals.isTicking = true; // rAF technique implementation that.globals.lastScrollX = window.scrollX || window.pageXOffset || document.documentElement.scrollLeft; that.globals.lastScrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop; that.globals.rafID = that.requestAnimFrame(that.proxy(function () { if(this.globals){ this.setViewport(); this.globals.isTicking = false; this.cancelAnimFrame(this.globals.rafID); } }, that)); } } }); } }, contains:function(parentNode,childNode){ if('contains' in parentNode) { return parentNode.contains(childNode); } else { return parentNode.compareDocumentPosition(childNode) % 16; } }, /** * cross browser event bubbling (propagation) prevention handler * @return void */ stopBubbling: function (e) { if (typeof e.stopPropagation !== "undefined") { e.stopPropagation(); } else if (typeof e.cancelBubble !== "undefined") { e.cancelBubble = true; } if (typeof e.preventDefault !== "undefined") { e.preventDefault(); } e.returnValue = false; return false; }, /** * Delays a multiple triggered method execution after the last one has been triggered * @return [function] given callback execution promise/result */ debounce: function (func, wait, immediate) { return function () { var context = this, args = arguments; if(context.globals){ var later = function () { if(context.globals){ context.globals.throttleTimeout = null; if (!immediate) func.apply(context, args); } }; var callNow = immediate && !context.globals.throttleTimeout; clearTimeout(context.globals.throttleTimeout); context.globals.throttleTimeout = setTimeout(later, wait); if (callNow) func.apply(context, args); } }; }, /** * Mimics jQuery.proxy */ proxy: function(callback, scope) { return callback.bind(scope); }, extend: function(defaults, options) { var extended = {}; copyObj(extended, defaults); copyObj(extended, options); function copyObj(to, from) { for (var key in from) { if (Object.prototype.hasOwnProperty.call(from, key)) { to[key] = from[key]; } } } return extended; }, /** * javascript rAF implementation for cross browser compatibility */ requestAnimFrame: function (callback) { if (typeof window.requestAnimationFrame === "function") return requestAnimationFrame(callback); if (typeof window.webkitRequestAnimationFrame === "function") return webkitRequestAnimationFrame(callback); if (typeof window.mozRequestAnimationFrame === "function") return mozRequestAnimationFrame(callback); return setTimeout(callback, 100 / 6); }, /** * javascript cAF implementation for cross browser compatibility */ cancelAnimFrame: function (id) { if (typeof window.cancelAnimationFrame === "function") return cancelAnimationFrame(id); if (typeof window.webkitCancelAnimationFrame === "function") return webkitCancelAnimationFrame(id); if (typeof window.mozCancelAnimationFrame === "function") return mozCancelAnimationFrame(id); return clearTimeout(id); }, /** * Attaches keyboard events if enabled * @return void */ addKeyboardEvents: function () { if (this.config.enableKeyboard) { var keyDownEvent = function (event) { var keycode = (event.which) ? event.which : event.keyCode; if (this.globals.keyboardHoverDate === null) { if (this.config.startDate === null) { this.globals.keyboardHoverDate = moment({ day: 1, month: parseInt(this.calendars.first().getAttribute("data-month")) }).middleOfDay(); } else { this.globals.keyboardHoverDate = this.config.startDate.clone().middleOfDay(); } } else { this.globals.keyboardHoverDate.middleOfDay(); } var shouldReDraw = false, shouldPrevent = false; switch (keycode) { case 37: // left this.globals.keyboardHoverDate.add(-1, "day"); shouldReDraw = true; shouldPrevent = true; break; case 38: // top this.globals.keyboardHoverDate.add(-1, "week"); shouldReDraw = true; shouldPrevent = true; break; case 39: // right this.globals.keyboardHoverDate.add(1, "day"); shouldReDraw = true; shouldPrevent = true; break; case 40: // bottom this.globals.keyboardHoverDate.add(1, "week"); shouldReDraw = true; shouldPrevent = true; break; case 32: // space this.input.querySelectorAll(".caleran-day[data-value='" + this.globals.keyboardHoverDate.middleOfDay().unix() + "']")[0].click(); shouldReDraw = false; shouldPrevent = true; break; case 33: // page up if (event.shiftKey) { this.globals.keyboardHoverDate.add(-1, "years"); } else { this.globals.keyboardHoverDate.add(-1, "months"); } shouldReDraw = true; shouldPrevent = true; break; case 34: // page down if (event.shiftKey) { this.globals.keyboardHoverDate.add(1, "years"); } else { this.globals.keyboardHoverDate.add(1, "months"); } shouldReDraw = true; shouldPrevent = true; break; case 27: // esc case 9: // tab this.hideDropdown(event); break; case 36: // home if (event.shiftKey) { this.globals.keyboardHoverDate = moment().middleOfDay(); shouldReDraw = true; shouldPrevent = true; } break; } if (shouldReDraw || shouldPrevent) { this.globals.keyboardHoverDate = this.globals.keyboardHoverDate.middleOfDay(); var days = this.input.querySelectorAll(".caleran-day[data-value]"); if (this.globals.keyboardHoverDate.isBefore(moment.unix(days[0].getAttribute("data-value")), "day") || this.globals.keyboardHoverDate.isAfter(moment.unix(days[days.length - 1].getAttribute("data-value")), "day")) { this.globals.currentDate = this.globals.keyboardHoverDate.clone().startOfMonth(); this.reDrawCalendars(); shouldReDraw = false; } if (shouldReDraw) { this.globals.hoverDate = null; this.reDrawCells(); } if (shouldPrevent) return this.stopBubbling(event); return false; } }.bind(this); this.rebindEvent(this.elem, "keydown", keyDownEvent, false); this.rebindEvent(this.container, "keydown", keyDownEvent, false); } }, /** * Destroys the instance */ destroy: function () { if (this.config.onBeforeDestroy(this)) { this.events.remove(this.container, "caleran:resize"); if (this.config.inline) { this.input.parentNode.removeChild(this.input); if (this.globals.isMobile) this.unwrap(this.elem, "caleran-container-mobile"); else this.unwrap(this.elem, "caleran-container"); this.elem.type = 'text'; } else { this.container.parentNode.removeChild(this.container); } this.events.remove(document, this.globals.documentEvent); this.elem.caleran = null; this.config.ondestroy(this); var i = null; for(i in this.config) this.config[i] = null; this.config = null; for(i in this.globals) this.globals[i] = null; this.globals = null; } }, unwrap: function (item, parentClass) { if(item && item.parentNode && item.parentNode.classList.contains(parentClass)) { item.parentNode.insertAdjacentHTML("beforebegin", item.parentNode.innerHTML); var wrapper = item.parentNode.parentNode.querySelector("." + parentClass); wrapper.parentNode.removeChild(wrapper); } }, /** * Detects if the browser is mobile * @return {boolean} if the browser is mobile or not */ checkMobile: function () { return window.matchMedia("only screen and (max-width: " + this.config.mobileBreakpoint + "px)").matches; }, /** * Converts the given datetime variable to momentjs object * @param {mixed} datetime the datetime variable to convert to moment object * @return {momentobject} the converted variable */ fixDateTime: function(datetime){ if(datetime != null && moment.isMoment(datetime) == false){ if(typeof datetime === "string"){ datetime = moment(datetime, this.config.format).locale(this.config.locale); }else{ datetime = moment(datetime).locale(this.config.locale); } } return datetime; }, /** * set start date & time programmatically * @param {moment object | js Date object | ISO Datetime string} datetime the value to be set */ setStart: function (datetime) { var datetimeConverted = this.fixDateTime(datetime); if (this.isDisabled(datetimeConverted) === false && moment(datetimeConverted).isValid()) { this.config.startDate = moment(datetimeConverted); this.refreshValues(); } }, /** * set end date & time programmatically * @param {moment object | js Date object | ISO Datetime string} datetime the value to be set */ setEnd: function (datetime) { var datetimeConverted = this.fixDateTime(datetime); if (this.isDisabled(datetimeConverted) === false && moment(datetimeConverted).isValid()) { this.config.endDate = moment(datetimeConverted); this.refreshValues(); } }, /** * set min date & time programmatically * @param {moment object | js Date object | ISO Datetime string} datetime the value to be set */ setMinDate: function (datetime) { var datetimeConverted = this.fixDateTime(datetime); if (moment(datetimeConverted).isValid()) { this.config.minDate = moment(datetimeConverted); this.refreshValues(); } }, /** * set max date & time programmatically * @param {moment object | js Date object | ISO Datetime string} datetime the value to be set */ setMaxDate: function (datetime) { var datetimeConverted = this.fixDateTime(datetime); if (moment(datetimeConverted).isValid()) { this.config.maxDate = moment(datetimeConverted); this.refreshValues(); } }, /** * Sets the displayed month and year * @param {moment object | js Date object | ISO Datetime string} datetime the value to be set */ setDisplayDate: function (datetime) { var datetimeConverted = this.fixDateTime(datetime); if (moment(datetimeConverted).isValid()) { this.globals.currentDate = moment(datetimeConverted); this.reDrawCalendars(); } }, /** * private method to reset the startdate and enddate to the input * @returns void */ refreshValues: function () { var backup = this.globals.delayInputUpdate; this.globals.delayInputUpdate = false; this.validateDates(); this.updateInput(); this.globals.delayInputUpdate = backup; this.reDrawCells(); }, eventsPrototype: function(){ return { instanceId: null, init: function(id){ this.instanceId = id; }, add: function(target, event, callback, once) { var that = this; // create eventbag if it doesn't exist target.eventsList = target.eventsList || {}; event += ".caleran_" + this.instanceId; var callbackName = event + "__" + Object.keys(target.eventsList).filter(function(key){ return key.indexOf(event + "__") === 0; }).length; if(once) { target.eventsList[callbackName] = function(e){ callback(e); that.remove(target, event, callback); }; } else { target.eventsList[callbackName] = function(e){ callback(e); }; } var genericEvent = event.split(/\./)[0]; if(target.addEventListener) { target.addEventListener(genericEvent, target.eventsList[callbackName], false); } else if (target.attachEvent) { target.attachEvent("on" + genericEvent, target.eventsList[callbackName]); } else { target["on" + genericEvent] = target.eventsList[callbackName]; } }, remove: function(target, event, callback) { if(typeof target.eventsList === "object"){ var eventKeys = []; event += ".caleran_" + this.instanceId; eventKeys = Object.keys(target.eventsList).filter(function(key){ return key.indexOf(event + "__") === 0; }); for(var ek = 0; ek < eventKeys.length; ek++){ var eventKey = eventKeys[ek]; var genericEvent = event.split(/\./)[0]; if(typeof target.removeEventListener === "function") { target.removeEventListener(genericEvent, target.eventsList[eventKey]); } else if(target.detachEvent) { target.detachEvent("on" + genericEvent, target.eventsList[eventKey]); } else { target["on" + genericEvent] = null; } delete target.eventsList[eventKey]; } } else { if (typeof target.removeEventListener === "function") { target.removeEventListener(event, null); } else if(target.detachEvent) { target.detachEvent("on" + event, null); } } }, dispatch: function(target, event) { if(typeof target.eventsList === "object"){ event += ".caleran_" + this.instanceId; var eventKeys = Object.keys(target.eventsList).filter(function(key){ return key.indexOf(event + "__") == 0; }); for(var ek = 0; ek < eventKeys.length; ek++){ var eventKey = eventKeys[ek]; var genericEvent = eventKey.split(/\./)[0]; this.triggerEvent(target, genericEvent); }; } else { this.triggerEvent(target, event); } }, triggerEvent: function(el, eventName) { var event; if(document.createEvent){ event = document.createEvent('HTMLEvents'); event.initEvent(eventName, false, true); }else if(document.createEventObject){// IE < 9 event = document.createEventObject(); event.eventType = eventName; } event.eventName = eventName; if(el.dispatchEvent){ el.dispatchEvent(event); }else if(el.fireEvent && htmlEvents['on'+eventName]){// IE < 9 el.fireEvent('on'+event.eventType,event);// can trigger only real event (e.g. 'click') }else if(el[eventName]){ el[eventName](); }else if(el['on'+eventName]){ el['on'+eventName](); } } }; } }; caleranPlugin.defaults = caleranPlugin.prototype.defaults; /** * The main handler of caleran plugin * @param {object} options javascript object which contains element specific or range specific options * @return {caleran} plugin reference */ window.caleran = function (selector, options) { var i = 0; if(typeof selector == "string"){ var items = document.querySelectorAll(selector); for(i = 0; i < items.length; i++){ var item = items[i]; item.caleran = new caleranPlugin(item, options).init(); }; }else if(typeof selector == "object"){ if(NodeList.prototype.isPrototypeOf(selector)){ for(i = 0; i < selector.length; i++) { selector[i].caleran = new caleranPlugin(selector[i], options).init(); } }else{ selector.caleran = new caleranPlugin(selector, options).init(); } } }; /** * add middleOfDay method to moment.js to set 12:00:00 for the current moment * @return {object} modified momentjs instance. */ if (typeof moment.fn.middleOfDay !== "function") { moment.fn.middleOfDay = function () { this.hours(12).minutes(0).seconds(0); return this; }; moment.fn.startOfMonth = function () { this.middleOfDay().date(1); return this; }; } })(window, document);