UNPKG

@pi0/framework7

Version:

Full featured mobile HTML framework for building iOS & Android apps

1,314 lines (1,243 loc) 45.5 kB
import $ from 'dom7'; import Utils from '../../utils/utils'; import Framework7Class from '../../utils/class'; class Calendar extends Framework7Class { constructor(app, params = {}) { super(params, [app]); const calendar = this; calendar.params = Utils.extend({}, app.params.calendar, params); let $containerEl; if (calendar.params.containerEl) { $containerEl = $(calendar.params.containerEl); if ($containerEl.length === 0) return calendar; } let $inputEl; if (calendar.params.inputEl) { $inputEl = $(calendar.params.inputEl); } let view; if ($inputEl) { view = $inputEl.parents('.view').length && $inputEl.parents('.view')[0].f7View; } if (!view) view = app.views.main; const isHorizontal = calendar.params.direction === 'horizontal'; let inverter = 1; if (isHorizontal) { inverter = app.rtl ? -1 : 1; } Utils.extend(calendar, { app, $containerEl, containerEl: $containerEl && $containerEl[0], inline: $containerEl && $containerEl.length > 0, $inputEl, inputEl: $inputEl && $inputEl[0], initialized: false, opened: false, url: calendar.params.url, isHorizontal, inverter, view, animating: false, }); function onInputClick() { calendar.open(); } function onInputFocus(e) { e.preventDefault(); } function onHtmlClick(e) { const $targetEl = $(e.target); if (calendar.isPopover()) return; if (!calendar.opened) return; if ($targetEl.closest('[class*="backdrop"]').length) return; if ($inputEl && $inputEl.length > 0) { if ($targetEl[0] !== $inputEl[0] && $targetEl.closest('.sheet-modal, .calendar-modal').length === 0) { calendar.close(); } } else if ($(e.target).closest('.sheet-modal, .calendar-modal').length === 0) { calendar.close(); } } // Events Utils.extend(calendar, { attachInputEvents() { calendar.$inputEl.on('click', onInputClick); if (calendar.params.inputReadOnly) { calendar.$inputEl.on('focus mousedown', onInputFocus); } }, detachInputEvents() { calendar.$inputEl.off('click', onInputClick); if (calendar.params.inputReadOnly) { calendar.$inputEl.off('focus mousedown', onInputFocus); } }, attachHtmlEvents() { app.on('click', onHtmlClick); }, detachHtmlEvents() { app.off('click', onHtmlClick); }, }); calendar.attachCalendarEvents = function attachCalendarEvents() { let allowItemClick = true; let isTouched; let isMoved; let touchStartX; let touchStartY; let touchCurrentX; let touchCurrentY; let touchStartTime; let touchEndTime; let currentTranslate; let wrapperWidth; let wrapperHeight; let percentage; let touchesDiff; let isScrolling; const { $el, $wrapperEl } = calendar; function handleTouchStart(e) { if (isMoved || isTouched) return; isTouched = true; touchStartX = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX; touchCurrentX = touchStartX; touchStartY = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY; touchCurrentY = touchStartY; touchStartTime = (new Date()).getTime(); percentage = 0; allowItemClick = true; isScrolling = undefined; currentTranslate = calendar.monthsTranslate; } function handleTouchMove(e) { if (!isTouched) return; const { isHorizontal: isH } = calendar; touchCurrentX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX; touchCurrentY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY; if (typeof isScrolling === 'undefined') { isScrolling = !!(isScrolling || Math.abs(touchCurrentY - touchStartY) > Math.abs(touchCurrentX - touchStartX)); } if (isH && isScrolling) { isTouched = false; return; } e.preventDefault(); if (calendar.animating) { isTouched = false; return; } allowItemClick = false; if (!isMoved) { // First move isMoved = true; wrapperWidth = $wrapperEl[0].offsetWidth; wrapperHeight = $wrapperEl[0].offsetHeight; $wrapperEl.transition(0); } touchesDiff = isH ? touchCurrentX - touchStartX : touchCurrentY - touchStartY; percentage = touchesDiff / (isH ? wrapperWidth : wrapperHeight); currentTranslate = ((calendar.monthsTranslate * calendar.inverter) + percentage) * 100; // Transform wrapper $wrapperEl.transform(`translate3d(${isH ? currentTranslate : 0}%, ${isH ? 0 : currentTranslate}%, 0)`); } function handleTouchEnd() { if (!isTouched || !isMoved) { isTouched = false; isMoved = false; return; } isTouched = false; isMoved = false; touchEndTime = new Date().getTime(); if (touchEndTime - touchStartTime < 300) { if (Math.abs(touchesDiff) < 10) { calendar.resetMonth(); } else if (touchesDiff >= 10) { if (app.rtl) calendar.nextMonth(); else calendar.prevMonth(); } else if (app.rtl) calendar.prevMonth(); else calendar.nextMonth(); } else if (percentage <= -0.5) { if (app.rtl) calendar.prevMonth(); else calendar.nextMonth(); } else if (percentage >= 0.5) { if (app.rtl) calendar.nextMonth(); else calendar.prevMonth(); } else { calendar.resetMonth(); } // Allow click setTimeout(() => { allowItemClick = true; }, 100); } function handleDayClick(e) { if (!allowItemClick) return; let $dayEl = $(e.target).parents('.calendar-day'); if ($dayEl.length === 0 && $(e.target).hasClass('calendar-day')) { $dayEl = $(e.target); } if ($dayEl.length === 0) return; if ($dayEl.hasClass('calendar-day-disabled')) return; if (!calendar.params.rangePicker) { if ($dayEl.hasClass('calendar-day-next')) calendar.nextMonth(); if ($dayEl.hasClass('calendar-day-prev')) calendar.prevMonth(); } const dateYear = $dayEl.attr('data-year'); const dateMonth = $dayEl.attr('data-month'); const dateDay = $dayEl.attr('data-day'); calendar.emit( 'local::dayClick calendarDayClick', calendar, $dayEl[0], dateYear, dateMonth, dateDay ); if (!$dayEl.hasClass('calendar-day-selected') || calendar.params.multiple || calendar.params.rangePicker) { calendar.addValue(new Date(dateYear, dateMonth, dateDay, 0, 0, 0)); } if (calendar.params.closeOnSelect) { if ( (calendar.params.rangePicker && calendar.value.length === 2) || !calendar.params.rangePicker ) { calendar.close(); } } } function onNextMonthClick() { calendar.nextMonth(); } function onPrevMonthClick() { calendar.prevMonth(); } function onNextYearClick() { calendar.nextYear(); } function onPrevYearClick() { calendar.prevYear(); } const passiveListener = app.touchEvents.start === 'touchstart' && app.support.passiveListener ? { passive: true, capture: false } : false; // Selectors clicks $el.find('.calendar-prev-month-button').on('click', onPrevMonthClick); $el.find('.calendar-next-month-button').on('click', onNextMonthClick); $el.find('.calendar-prev-year-button').on('click', onPrevYearClick); $el.find('.calendar-next-year-button').on('click', onNextYearClick); // Day clicks $wrapperEl.on('click', handleDayClick); // Touch events if (process.env.TARGET !== 'desktop') { if (calendar.params.touchMove) { $wrapperEl.on(app.touchEvents.start, handleTouchStart, passiveListener); app.on('touchmove:active', handleTouchMove); app.on('touchend:passive', handleTouchEnd); } } calendar.detachCalendarEvents = function detachCalendarEvents() { $el.find('.calendar-prev-month-button').off('click', onPrevMonthClick); $el.find('.calendar-next-month-button').off('click', onNextMonthClick); $el.find('.calendar-prev-year-button').off('click', onPrevYearClick); $el.find('.calendar-next-year-button').off('click', onNextYearClick); $wrapperEl.off('click', handleDayClick); if (process.env.TARGET !== 'desktop') { if (calendar.params.touchMove) { $wrapperEl.off(app.touchEvents.start, handleTouchStart, passiveListener); app.off('touchmove:active', handleTouchMove); app.off('touchend:passive', handleTouchEnd); } } }; }; calendar.init(); return calendar; } initInput() { const calendar = this; if (!calendar.$inputEl) return; if (calendar.params.inputReadOnly) calendar.$inputEl.prop('readOnly', true); } isPopover() { const calendar = this; const { app, modal, params } = calendar; if (params.openIn === 'sheet') return false; if (modal && modal.type !== 'popover') return false; if (!calendar.inline && calendar.inputEl) { if (params.openIn === 'popover') return true; else if (app.device.ios) { return !!app.device.ipad; } else if (app.width >= 768) { return true; } } return false; } formatDate(d) { const calendar = this; const date = new Date(d); const year = date.getFullYear(); const month = date.getMonth(); const month1 = month + 1; const day = date.getDate(); const weekDay = date.getDay(); const { dateFormat, monthNames, monthNamesShort, dayNames, dayNamesShort } = calendar.params; return dateFormat .replace(/yyyy/g, year) .replace(/yy/g, String(year).substring(2)) .replace(/mm/g, month1 < 10 ? `0${month1}` : month1) .replace(/m(\W+)/g, `${month1}$1`) .replace(/MM/g, monthNames[month]) .replace(/M(\W+)/g, `${monthNamesShort[month]}$1`) .replace(/dd/g, day < 10 ? `0${day}` : day) .replace(/d(\W+)/g, `${day}$1`) .replace(/DD/g, dayNames[weekDay]) .replace(/D(\W+)/g, `${dayNamesShort[weekDay]}$1`); } formatValue() { const calendar = this; const { value } = calendar; if (calendar.params.formatValue) { return calendar.params.formatValue.call(calendar, value); } return value .map(v => calendar.formatDate(v)) .join(calendar.params.rangePicker ? ' - ' : ', '); } addValue(newValue) { const calendar = this; const { multiple, rangePicker } = calendar.params; if (multiple) { if (!calendar.value) calendar.value = []; let inValuesIndex; for (let i = 0; i < calendar.value.length; i += 1) { if (new Date(newValue).getTime() === new Date(calendar.value[i]).getTime()) { inValuesIndex = i; } } if (typeof inValuesIndex === 'undefined') { calendar.value.push(newValue); } else { calendar.value.splice(inValuesIndex, 1); } calendar.updateValue(); } else if (rangePicker) { if (!calendar.value) calendar.value = []; if (calendar.value.length === 2 || calendar.value.length === 0) { calendar.value = []; } if (calendar.value[0] !== newValue) calendar.value.push(newValue); else calendar.value = []; calendar.value.sort((a, b) => a - b); calendar.updateValue(); } else { calendar.value = [newValue]; calendar.updateValue(); } } setValue(values) { const calendar = this; calendar.value = values; calendar.updateValue(); } getValue() { const calendar = this; return calendar.value; } updateValue(onlyHeader) { const calendar = this; const { $el, $wrapperEl, $inputEl, value, params, } = calendar; let i; if ($el && $el.length > 0) { $wrapperEl.find('.calendar-day-selected').removeClass('calendar-day-selected'); let valueDate; if (params.rangePicker && value.length === 2) { for (i = new Date(value[0]).getTime(); i <= new Date(value[1]).getTime(); i += 24 * 60 * 60 * 1000) { valueDate = new Date(i); $wrapperEl.find(`.calendar-day[data-date="${valueDate.getFullYear()}-${valueDate.getMonth()}-${valueDate.getDate()}"]`).addClass('calendar-day-selected'); } } else { for (i = 0; i < calendar.value.length; i += 1) { valueDate = new Date(value[i]); $wrapperEl.find(`.calendar-day[data-date="${valueDate.getFullYear()}-${valueDate.getMonth()}-${valueDate.getDate()}"]`).addClass('calendar-day-selected'); } } } calendar.emit('local::change calendarChange', calendar, value); if (($inputEl && $inputEl.length) || params.header) { const inputValue = calendar.formatValue(value); if (params.header && $el && $el.length) { $el.find('.calendar-selected-date').text(inputValue); } if ($inputEl && $inputEl.length && !onlyHeader) { $inputEl.val(inputValue); $inputEl.trigger('change'); } } } updateCurrentMonthYear(dir) { const calendar = this; const { $months, $el, params } = calendar; if (typeof dir === 'undefined') { calendar.currentMonth = parseInt($months.eq(1).attr('data-month'), 10); calendar.currentYear = parseInt($months.eq(1).attr('data-year'), 10); } else { calendar.currentMonth = parseInt($months.eq(dir === 'next' ? ($months.length - 1) : 0).attr('data-month'), 10); calendar.currentYear = parseInt($months.eq(dir === 'next' ? ($months.length - 1) : 0).attr('data-year'), 10); } $el.find('.current-month-value').text(params.monthNames[calendar.currentMonth]); $el.find('.current-year-value').text(calendar.currentYear); } update() { const calendar = this; const { currentYear, currentMonth, $wrapperEl } = calendar; const currentDate = new Date(currentYear, currentMonth); const prevMonthHtml = calendar.renderMonth(currentDate, 'prev'); const currentMonthHtml = calendar.renderMonth(currentDate); const nextMonthHtml = calendar.renderMonth(currentDate, 'next'); $wrapperEl .html(`${prevMonthHtml}${currentMonthHtml}${nextMonthHtml}`) .transform('translate3d(0,0,0)'); calendar.$months = $wrapperEl.find('.calendar-month'); calendar.monthsTranslate = 0; calendar.setMonthsTranslate(); calendar.$months.each((index, monthEl) => { calendar.emit( 'local::monthAdd calendarMonthAdd', monthEl ); }); } onMonthChangeStart(dir) { const calendar = this; const { $months, currentYear, currentMonth } = calendar; calendar.updateCurrentMonthYear(dir); $months.removeClass('calendar-month-current calendar-month-prev calendar-month-next'); const currentIndex = dir === 'next' ? $months.length - 1 : 0; $months.eq(currentIndex).addClass('calendar-month-current'); $months.eq(dir === 'next' ? currentIndex - 1 : currentIndex + 1).addClass(dir === 'next' ? 'calendar-month-prev' : 'calendar-month-next'); calendar.emit( 'local::monthYearChangeStart calendarMonthYearChangeStart', calendar, currentYear, currentMonth ); } onMonthChangeEnd(dir, rebuildBoth) { const calendar = this; const { currentYear, currentMonth, $wrapperEl, monthsTranslate } = calendar; calendar.animating = false; let nextMonthHtml; let prevMonthHtml; let currentMonthHtml; $wrapperEl .find('.calendar-month:not(.calendar-month-prev):not(.calendar-month-current):not(.calendar-month-next)') .remove(); if (typeof dir === 'undefined') { dir = 'next'; // eslint-disable-line rebuildBoth = true; // eslint-disable-line } if (!rebuildBoth) { currentMonthHtml = calendar.renderMonth(new Date(currentYear, currentMonth), dir); } else { $wrapperEl.find('.calendar-month-next, .calendar-month-prev').remove(); prevMonthHtml = calendar.renderMonth(new Date(currentYear, currentMonth), 'prev'); nextMonthHtml = calendar.renderMonth(new Date(currentYear, currentMonth), 'next'); } if (dir === 'next' || rebuildBoth) { $wrapperEl.append(currentMonthHtml || nextMonthHtml); } if (dir === 'prev' || rebuildBoth) { $wrapperEl.prepend(currentMonthHtml || prevMonthHtml); } const $months = $wrapperEl.find('.calendar-month'); calendar.$months = $months; calendar.setMonthsTranslate(monthsTranslate); calendar.emit( 'local::monthAdd calendarMonthAdd', calendar, dir === 'next' ? $months.eq($months.length - 1)[0] : $months.eq(0)[0] ); calendar.emit( 'local::monthYearChangeEnd calendarMonthYearChangeEnd', calendar, currentYear, currentMonth ); } setMonthsTranslate(translate) { const calendar = this; const { $months, isHorizontal: isH, inverter } = calendar; // eslint-disable-next-line translate = translate || calendar.monthsTranslate || 0; if (typeof calendar.monthsTranslate === 'undefined') { calendar.monthsTranslate = translate; } $months.removeClass('calendar-month-current calendar-month-prev calendar-month-next'); const prevMonthTranslate = -(translate + 1) * 100 * inverter; const currentMonthTranslate = -translate * 100 * inverter; const nextMonthTranslate = -(translate - 1) * 100 * inverter; $months.eq(0) .transform(`translate3d(${isH ? prevMonthTranslate : 0}%, ${isH ? 0 : prevMonthTranslate}%, 0)`) .addClass('calendar-month-prev'); $months.eq(1) .transform(`translate3d(${isH ? currentMonthTranslate : 0}%, ${isH ? 0 : currentMonthTranslate}%, 0)`) .addClass('calendar-month-current'); $months.eq(2) .transform(`translate3d(${isH ? nextMonthTranslate : 0}%, ${isH ? 0 : nextMonthTranslate}%, 0)`) .addClass('calendar-month-next'); } nextMonth(transition) { const calendar = this; const { params, $wrapperEl, inverter, isHorizontal: isH } = calendar; if (typeof transition === 'undefined' || typeof transition === 'object') { transition = ''; // eslint-disable-line if (!params.animate) transition = 0; // eslint-disable-line } const nextMonth = parseInt(calendar.$months.eq(calendar.$months.length - 1).attr('data-month'), 10); const nextYear = parseInt(calendar.$months.eq(calendar.$months.length - 1).attr('data-year'), 10); const nextDate = new Date(nextYear, nextMonth); const nextDateTime = nextDate.getTime(); const transitionEndCallback = !calendar.animating; if (params.maxDate) { if (nextDateTime > new Date(params.maxDate).getTime()) { calendar.resetMonth(); return; } } calendar.monthsTranslate -= 1; if (nextMonth === calendar.currentMonth) { const nextMonthTranslate = -(calendar.monthsTranslate) * 100 * inverter; const nextMonthHtml = $(calendar.renderMonth(nextDateTime, 'next')) .transform(`translate3d(${isH ? nextMonthTranslate : 0}%, ${isH ? 0 : nextMonthTranslate}%, 0)`) .addClass('calendar-month-next'); $wrapperEl.append(nextMonthHtml[0]); calendar.$months = $wrapperEl.find('.calendar-month'); calendar.emit( 'local::monthAdd calendarMonthAdd', calendar.$months.eq(calendar.$months.length - 1)[0] ); } calendar.animating = true; calendar.onMonthChangeStart('next'); const translate = (calendar.monthsTranslate * 100) * inverter; $wrapperEl.transition(transition).transform(`translate3d(${isH ? translate : 0}%, ${isH ? 0 : translate}%, 0)`); if (transitionEndCallback) { $wrapperEl.transitionEnd(() => { calendar.onMonthChangeEnd('next'); }); } if (!params.animate) { calendar.onMonthChangeEnd('next'); } } prevMonth(transition) { const calendar = this; const { params, $wrapperEl, inverter, isHorizontal: isH } = calendar; if (typeof transition === 'undefined' || typeof transition === 'object') { transition = ''; // eslint-disable-line if (!params.animate) transition = 0; // eslint-disable-line } const prevMonth = parseInt(calendar.$months.eq(0).attr('data-month'), 10); const prevYear = parseInt(calendar.$months.eq(0).attr('data-year'), 10); const prevDate = new Date(prevYear, prevMonth + 1, -1); const prevDateTime = prevDate.getTime(); const transitionEndCallback = !calendar.animating; if (params.minDate) { if (prevDateTime < new Date(params.minDate).getTime()) { calendar.resetMonth(); return; } } calendar.monthsTranslate += 1; if (prevMonth === calendar.currentMonth) { const prevMonthTranslate = -(calendar.monthsTranslate) * 100 * inverter; const prevMonthHtml = $(calendar.renderMonth(prevDateTime, 'prev')) .transform(`translate3d(${isH ? prevMonthTranslate : 0}%, ${isH ? 0 : prevMonthTranslate}%, 0)`) .addClass('calendar-month-prev'); $wrapperEl.prepend(prevMonthHtml[0]); calendar.$months = $wrapperEl.find('.calendar-month'); calendar.emit( 'local::monthAdd calendarMonthAdd', calendar.$months.eq(0)[0] ); } calendar.animating = true; calendar.onMonthChangeStart('prev'); const translate = (calendar.monthsTranslate * 100) * inverter; $wrapperEl .transition(transition) .transform(`translate3d(${isH ? translate : 0}%, ${isH ? 0 : translate}%, 0)`); if (transitionEndCallback) { $wrapperEl.transitionEnd(() => { calendar.onMonthChangeEnd('prev'); }); } if (!params.animate) { calendar.onMonthChangeEnd('prev'); } } resetMonth(transition = '') { const calendar = this; const { $wrapperEl, inverter, isHorizontal: isH, monthsTranslate } = calendar; const translate = (monthsTranslate * 100) * inverter; $wrapperEl .transition(transition) .transform(`translate3d(${isH ? translate : 0}%, ${isH ? 0 : translate}%, 0)`); } setYearMonth(year, month, transition) { const calendar = this; const { params, isHorizontal: isH, $wrapperEl, inverter } = calendar; if (typeof year === 'undefined') year = calendar.currentYear; if (typeof month === 'undefined') month = calendar.currentMonth; if (typeof transition === 'undefined' || typeof transition === 'object') { transition = ''; if (!params.animate) transition = 0; } let targetDate; if (year < calendar.currentYear) { targetDate = new Date(year, month + 1, -1).getTime(); } else { targetDate = new Date(year, month).getTime(); } if (params.maxDate && targetDate > new Date(params.maxDate).getTime()) { return false; } if (params.minDate && targetDate < new Date(params.minDate).getTime()) { return false; } const currentDate = new Date(calendar.currentYear, calendar.currentMonth).getTime(); const dir = targetDate > currentDate ? 'next' : 'prev'; const newMonthHTML = calendar.renderMonth(new Date(year, month)); calendar.monthsTranslate = calendar.monthsTranslate || 0; const prevTranslate = calendar.monthsTranslate; let monthTranslate; const transitionEndCallback = !calendar.animating; if (targetDate > currentDate) { // To next calendar.monthsTranslate -= 1; if (!calendar.animating) calendar.$months.eq(calendar.$months.length - 1).remove(); $wrapperEl.append(newMonthHTML); calendar.$months = $wrapperEl.find('.calendar-month'); monthTranslate = -(prevTranslate - 1) * 100 * inverter; calendar.$months .eq(calendar.$months.length - 1) .transform(`translate3d(${isH ? monthTranslate : 0}%, ${isH ? 0 : monthTranslate}%, 0)`) .addClass('calendar-month-next'); } else { // To prev calendar.monthsTranslate += 1; if (!calendar.animating) calendar.$months.eq(0).remove(); $wrapperEl.prepend(newMonthHTML); calendar.$months = $wrapperEl.find('.calendar-month'); monthTranslate = -(prevTranslate + 1) * 100 * inverter; calendar.$months .eq(0) .transform(`translate3d(${isH ? monthTranslate : 0}%, ${isH ? 0 : monthTranslate}%, 0)`) .addClass('calendar-month-prev'); } calendar.emit( 'local::monthAdd calendarMonthAdd', dir === 'next' ? calendar.$months.eq(calendar.$months.length - 1)[0] : calendar.$months.eq(0)[0] ); calendar.animating = true; calendar.onMonthChangeStart(dir); const wrapperTranslate = (calendar.monthsTranslate * 100) * inverter; $wrapperEl .transition(transition) .transform(`translate3d(${isH ? wrapperTranslate : 0}%, ${isH ? 0 : wrapperTranslate}%, 0)`); if (transitionEndCallback) { $wrapperEl.transitionEnd(() => { calendar.onMonthChangeEnd(dir, true); }); } if (!params.animate) { calendar.onMonthChangeEnd(dir); } } nextYear() { const calendar = this; calendar.setYearMonth(calendar.currentYear + 1); } prevYear() { const calendar = this; calendar.setYearMonth(calendar.currentYear - 1); } // eslint-disable-next-line dateInRange(dayDate, range) { let match = false; let i; if (!range) return false; if (Array.isArray(range)) { for (i = 0; i < range.length; i += 1) { if (range[i].from || range[i].to) { if (range[i].from && range[i].to) { if ((dayDate <= new Date(range[i].to).getTime()) && (dayDate >= new Date(range[i].from).getTime())) { match = true; } } else if (range[i].from) { if (dayDate >= new Date(range[i].from).getTime()) { match = true; } } else if (range[i].to) { if (dayDate <= new Date(range[i].to).getTime()) { match = true; } } } else if (dayDate === new Date(range[i]).getTime()) { match = true; } } } else if (range.from || range.to) { if (range.from && range.to) { if ((dayDate <= new Date(range.to).getTime()) && (dayDate >= new Date(range.from).getTime())) { match = true; } } else if (range.from) { if (dayDate >= new Date(range.from).getTime()) { match = true; } } else if (range.to) { if (dayDate <= new Date(range.to).getTime()) { match = true; } } } else if (typeof range === 'function') { match = range(new Date(dayDate)); } return match; } // eslint-disable-next-line daysInMonth(date) { const d = new Date(date); return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(); } renderMonths(date) { const calendar = this; if (calendar.params.renderMonths) { return calendar.params.renderMonths.call(calendar, date); } return ` <div class="calendar-months-wrapper"> ${calendar.renderMonth(date, 'prev')} ${calendar.renderMonth(date)} ${calendar.renderMonth(date, 'next')} </div> `.trim(); } renderMonth(d, offset) { const calendar = this; const { params, value } = calendar; if (params.renderMonth) { return params.renderMonth.call(calendar, d, offset); } let date = new Date(d); let year = date.getFullYear(); let month = date.getMonth(); if (offset === 'next') { if (month === 11) date = new Date(year + 1, 0); else date = new Date(year, month + 1, 1); } if (offset === 'prev') { if (month === 0) date = new Date(year - 1, 11); else date = new Date(year, month - 1, 1); } if (offset === 'next' || offset === 'prev') { month = date.getMonth(); year = date.getFullYear(); } const currentValues = []; const today = new Date().setHours(0, 0, 0, 0); const minDate = params.minDate ? new Date(params.minDate).getTime() : null; const maxDate = params.maxDate ? new Date(params.maxDate).getTime() : null; const rows = 6; const cols = 7; const daysInPrevMonth = calendar.daysInMonth(new Date(date.getFullYear(), date.getMonth()).getTime() - (10 * 24 * 60 * 60 * 1000)); const daysInMonth = calendar.daysInMonth(date); const minDayNumber = params.firstDay === 6 ? 0 : 1; let monthHtml = ''; let dayIndex = 0 + (params.firstDay - 1); let disabled; let hasEvent; let firstDayOfMonthIndex = new Date(date.getFullYear(), date.getMonth()).getDay(); if (firstDayOfMonthIndex === 0) firstDayOfMonthIndex = 7; if (value && value.length) { for (let i = 0; i < value.length; i += 1) { currentValues.push(new Date(value[i]).setHours(0, 0, 0, 0)); } } for (let row = 1; row <= rows; row += 1) { let rowHtml = ''; for (let col = 1; col <= cols; col += 1) { dayIndex += 1; let dayDate; let dayNumber = dayIndex - firstDayOfMonthIndex; let addClass = ''; if (row === 1 && col === 1 && dayNumber > minDayNumber && params.firstDay !== 1) { dayIndex -= 7; dayNumber = dayIndex - firstDayOfMonthIndex; } const weekDayIndex = ((col - 1) + params.firstDay > 6) ? ((col - 1 - 7) + params.firstDay) : ((col - 1) + params.firstDay); if (dayNumber < 0) { dayNumber = daysInPrevMonth + dayNumber + 1; addClass += ' calendar-day-prev'; dayDate = new Date(month - 1 < 0 ? year - 1 : year, month - 1 < 0 ? 11 : month - 1, dayNumber).getTime(); } else { dayNumber += 1; if (dayNumber > daysInMonth) { dayNumber -= daysInMonth; addClass += ' calendar-day-next'; dayDate = new Date(month + 1 > 11 ? year + 1 : year, month + 1 > 11 ? 0 : month + 1, dayNumber).getTime(); } else { dayDate = new Date(year, month, dayNumber).getTime(); } } // Today if (dayDate === today) addClass += ' calendar-day-today'; // Selected if (params.rangePicker && currentValues.length === 2) { if (dayDate >= currentValues[0] && dayDate <= currentValues[1]) addClass += ' calendar-day-selected'; } else if (currentValues.indexOf(dayDate) >= 0) addClass += ' calendar-day-selected'; // Weekend if (params.weekendDays.indexOf(weekDayIndex) >= 0) { addClass += ' calendar-day-weekend'; } // Has Events hasEvent = false; if (params.events) { if (calendar.dateInRange(dayDate, params.events)) { hasEvent = true; } } if (hasEvent) { addClass += ' calendar-day-has-events'; } // Custom Ranges if (params.rangesClasses) { for (let k = 0; k < params.rangesClasses.length; k += 1) { if (calendar.dateInRange(dayDate, params.rangesClasses[k].range)) { addClass += ` ${params.rangesClasses[k].cssClass}`; } } } // Disabled disabled = false; if ((minDate && dayDate < minDate) || (maxDate && dayDate > maxDate)) { disabled = true; } if (params.disabled) { if (calendar.dateInRange(dayDate, params.disabled)) { disabled = true; } } if (disabled) { addClass += ' calendar-day-disabled'; } dayDate = new Date(dayDate); const dayYear = dayDate.getFullYear(); const dayMonth = dayDate.getMonth(); rowHtml += ` <div data-year="${dayYear}" data-month="${dayMonth}" data-day="${dayNumber}" class="calendar-day${addClass}" data-date="${dayYear}-${dayMonth}-${dayNumber}"> <span>${dayNumber}</span> </div>`.trim(); } monthHtml += `<div class="calendar-row">${rowHtml}</div>`; } monthHtml = `<div class="calendar-month" data-year="${year}" data-month="${month}">${monthHtml}</div>`; return monthHtml; } renderWeekHeader() { const calendar = this; if (calendar.params.renderWeekHeader) { return calendar.params.renderWeekHeader.call(calendar); } const { params } = calendar; let weekDaysHtml = ''; for (let i = 0; i < 7; i += 1) { const dayIndex = (i + params.firstDay > 6) ? ((i - 7) + params.firstDay) : (i + params.firstDay); const dayName = params.dayNamesShort[dayIndex]; weekDaysHtml += `<div class="calendar-week-day">${dayName}</div>`; } return ` <div class="calendar-week-header"> ${weekDaysHtml} </div> `.trim(); } renderMonthSelector() { const calendar = this; const app = calendar.app; if (calendar.params.renderMonthSelector) { return calendar.params.renderMonthSelector.call(calendar); } const iconColor = app.theme === 'md' ? 'color-black' : ''; return ` <div class="calendar-month-selector"> <a href="#" class="link icon-only calendar-prev-month-button"> <i class="icon icon-prev ${iconColor}"></i> </a> <span class="current-month-value"></span> <a href="#" class="link icon-only calendar-next-month-button"> <i class="icon icon-next ${iconColor}"></i> </a> </div> `.trim(); } renderYearSelector() { const calendar = this; const app = calendar.app; if (calendar.params.renderYearSelector) { return calendar.params.renderYearSelector.call(calendar); } const iconColor = app.theme === 'md' ? 'color-black' : ''; return ` <div class="calendar-year-selector"> <a href="#" class="link icon-only calendar-prev-year-button"> <i class="icon icon-prev ${iconColor}"></i> </a> <span class="current-year-value"></span> <a href="#" class="link icon-only calendar-next-year-button"> <i class="icon icon-next ${iconColor}"></i> </a> </div> `.trim(); } renderHeader() { const calendar = this; if (calendar.params.renderHeader) { return calendar.params.renderHeader.call(calendar); } return ` <div class="calendar-header"> <div class="calendar-selected-date">${calendar.params.headerPlaceholder}</div> </div> `.trim(); } renderFooter() { const calendar = this; const app = calendar.app; if (calendar.params.renderFooter) { return calendar.params.renderFooter.call(calendar); } return ` <div class="calendar-footer"> <a href="#" class="${app.theme === 'md' ? 'button' : 'link'} calendar-close sheet-close popover-close">${calendar.params.toolbarCloseText}</a> </div> `.trim(); } renderToolbar() { const calendar = this; if (calendar.params.renderToolbar) { return calendar.params.renderToolbar.call(calendar, calendar); } return ` <div class="toolbar no-shadow"> <div class="toolbar-inner"> ${calendar.renderMonthSelector()} ${calendar.renderYearSelector()} </div> </div> `.trim(); } // eslint-disable-next-line renderInline() { const calendar = this; const { cssClass, toolbar, header, footer, rangePicker, weekHeader } = calendar.params; const { value } = calendar; const date = value && value.length ? value[0] : new Date().setHours(0, 0, 0); const inlineHtml = ` <div class="calendar calendar-inline ${rangePicker ? 'calendar-range' : ''} ${cssClass || ''}"> ${header ? calendar.renderHeader() : ''} ${toolbar ? calendar.renderToolbar() : ''} ${weekHeader ? calendar.renderWeekHeader() : ''} <div class="calendar-months"> ${calendar.renderMonths(date)} </div> ${footer ? calendar.renderFooter() : ''} </div> `.trim(); return inlineHtml; } renderCustomModal() { const calendar = this; const { cssClass, toolbar, header, footer, rangePicker, weekHeader } = calendar.params; const { value } = calendar; const date = value && value.length ? value[0] : new Date().setHours(0, 0, 0); const sheetHtml = ` <div class="calendar calendar-modal ${rangePicker ? 'calendar-range' : ''} ${cssClass || ''}"> ${header ? calendar.renderHeader() : ''} ${toolbar ? calendar.renderToolbar() : ''} ${weekHeader ? calendar.renderWeekHeader() : ''} <div class="calendar-months"> ${calendar.renderMonths(date)} </div> ${footer ? calendar.renderFooter() : ''} </div> `.trim(); return sheetHtml; } renderSheet() { const calendar = this; const { cssClass, toolbar, header, footer, rangePicker, weekHeader } = calendar.params; const { value } = calendar; const date = value && value.length ? value[0] : new Date().setHours(0, 0, 0); const sheetHtml = ` <div class="sheet-modal calendar calendar-sheet ${rangePicker ? 'calendar-range' : ''} ${cssClass || ''}"> ${header ? calendar.renderHeader() : ''} ${toolbar ? calendar.renderToolbar() : ''} ${weekHeader ? calendar.renderWeekHeader() : ''} <div class="sheet-modal-inner calendar-months"> ${calendar.renderMonths(date)} </div> ${footer ? calendar.renderFooter() : ''} </div> `.trim(); return sheetHtml; } renderPopover() { const calendar = this; const { cssClass, toolbar, header, footer, rangePicker, weekHeader } = calendar.params; const { value } = calendar; const date = value && value.length ? value[0] : new Date().setHours(0, 0, 0); const popoverHtml = ` <div class="popover calendar-popover"> <div class="popover-inner"> <div class="calendar ${rangePicker ? 'calendar-range' : ''} ${cssClass || ''}"> ${header ? calendar.renderHeader() : ''} ${toolbar ? calendar.renderToolbar() : ''} ${weekHeader ? calendar.renderWeekHeader() : ''} <div class="calendar-months"> ${calendar.renderMonths(date)} </div> ${footer ? calendar.renderFooter() : ''} </div> </div> </div> `.trim(); return popoverHtml; } render() { const calendar = this; const { params } = calendar; if (params.render) return params.render.call(calendar); if (!calendar.inline) { let modalType = params.openIn; if (modalType === 'auto') modalType = calendar.isPopover() ? 'popover' : 'sheet'; if (modalType === 'popover') return calendar.renderPopover(); else if (modalType === 'sheet') return calendar.renderSheet(); return calendar.renderCustomModal(); } return calendar.renderInline(); } onOpen() { const calendar = this; const { initialized, $el, app, $inputEl, inline, value, params } = calendar; calendar.opened = true; // Init main events calendar.attachCalendarEvents(); const updateValue = !value && params.value; // Set value if (!initialized) { if (value) calendar.setValue(value, 0); else if (params.value) { calendar.setValue(params.value, 0); } } else if (value) { calendar.setValue(value, 0); } // Update current month and year calendar.updateCurrentMonthYear(); // Set initial translate calendar.monthsTranslate = 0; calendar.setMonthsTranslate(); // Update input value if (updateValue) calendar.updateValue(); else if (app.theme === 'md' && value) calendar.updateValue(true); // Extra focus if (!inline && $inputEl.length && app.theme === 'md') { $inputEl.trigger('focus'); } calendar.initialized = true; calendar.$months.each((index, monthEl) => { calendar.emit('local::monthAdd calendarMonthAdd', monthEl); }); // Trigger events if ($el) { $el.trigger('calendar:open', calendar); } if ($inputEl) { $inputEl.trigger('calendar:open', calendar); } calendar.emit('local::open calendarOpen', calendar); } onOpened() { const calendar = this; if (calendar.$el) { calendar.$el.trigger('calendar:opened', calendar); } if (calendar.$inputEl) { calendar.$inputEl.trigger('calendar:opened', calendar); } calendar.emit('local::opened calendarOpened', calendar); } onClose() { const calendar = this; const app = calendar.app; if (calendar.$inputEl && app.theme === 'md') { calendar.$inputEl.trigger('blur'); } if (calendar.detachCalendarEvents) { calendar.detachCalendarEvents(); } if (calendar.$el) { calendar.$el.trigger('calendar:close', calendar); } if (calendar.$inputEl) { calendar.$inputEl.trigger('calendar:close', calendar); } calendar.emit('local::close calendarClose', calendar); } onClosed() { const calendar = this; calendar.opened = false; if (!calendar.inline) { Utils.nextTick(() => { if (calendar.modal && calendar.modal.el && calendar.modal.destroy) { if (!calendar.params.routableModals) { calendar.modal.destroy(); } } delete calendar.modal; }); } if (calendar.$el) { calendar.$el.trigger('calendar:closed', calendar); } if (calendar.$inputEl) { calendar.$inputEl.trigger('calendar:closed', calendar); } calendar.emit('local::closed calendarClosed', calendar); } open() { const calendar = this; const { app, opened, inline, $inputEl, params } = calendar; if (opened) return; if (inline) { calendar.$el = $(calendar.render()); calendar.$el[0].f7Calendar = calendar; calendar.$wrapperEl = calendar.$el.find('.calendar-months-wrapper'); calendar.$months = calendar.$wrapperEl.find('.calendar-month'); calendar.$containerEl.append(calendar.$el); calendar.onOpen(); calendar.onOpened(); return; } let modalType = params.openIn; if (modalType === 'auto') { modalType = calendar.isPopover() ? 'popover' : 'sheet'; } const modalContent = calendar.render(); const modalParams = { targetEl: $inputEl, scrollToEl: calendar.params.scrollToInput ? $inputEl : undefined, content: modalContent, backdrop: modalType !== 'sheet', on: { open() { const modal = this; calendar.modal = modal; calendar.$el = modalType === 'popover' ? modal.$el.find('.calendar') : modal.$el; calendar.$wrapperEl = calendar.$el.find('.calendar-months-wrapper'); calendar.$months = calendar.$wrapperEl.find('.calendar-month'); calendar.$el[0].f7Calendar = calendar; if (modalType === 'customModal') { $(calendar.$el).find('.calendar-close').once('click', () => { calendar.close(); }); } calendar.onOpen(); }, opened() { calendar.onOpened(); }, close() { calendar.onClose(); }, closed() { calendar.onClosed(); }, }, }; if (calendar.params.routableModals) { calendar.view.router.navigate(calendar.url, { createRoute: { path: calendar.url, [modalType]: modalParams, }, }); } else { calendar.modal = app[modalType].create(modalParams); calendar.modal.open(); } } close() { const calendar = this; const { opened, inline } = calendar; if (!opened) return; if (inline) { calendar.onClose(); calendar.onClosed(); return; } if (calendar.params.routableModals) { calendar.view.router.back(); } else { calendar.modal.close(); } } init() { const calendar = this; calendar.initInput(); if (calendar.inline) { calendar.open(); calendar.emit('local::init calendarInit', calendar); return; } if (!calendar.initialized && calendar.params.value) { calendar.setValue(calendar.params.value); } // Attach input Events if (calendar.$inputEl) { calendar.attachInputEvents(); } if (calendar.params.closeByOutsideClick) { calendar.attachHtmlEvents(); } calendar.emit('local::init calendarInit', calendar); } destroy() { const calendar = this; if (calendar.destroyed) return; const { $el } = calendar; calendar.emit('local::beforeDestroy calendarBeforeDestroy', calendar); if ($el) $el.trigger('calendar:beforedestroy', calendar); calendar.close(); // Detach Events if (calendar.$inputEl) { calendar.detachInputEvents(); } if (calendar.params.closeByOutsideClick) { calendar.detachHtmlEvents(); } if ($el && $el.length) delete calendar.$el[0].f7Calendar; Utils.deleteProps(calendar); calendar.destroyed = true; } } export default Calendar;