m-persian-datepicker
Version:
jQuery datepicker plugin work with Iranian calendar. (Jalali)
633 lines (571 loc) • 21.5 kB
JavaScript
let Template = require('./template');
let Helper = require('./helper');
let Mustache = require('mustache');
/**
* As its name suggests, all rendering works do in this object
*/
class View {
/**
*
* @param {Datepicker} model
* @return {View}
*/
constructor (model) {
/**
* @type {number}
*/
this.yearsViewCount = 12;
/**
*
* @type {Datepicker}
*/
this.model = model;
/**
*
* @type {null}
*/
this.rendered = null;
/**
*
* @type {null}
*/
this.$container = null;
/**
*
* @type {string}
*/
this.id = `persianDateInstance-${parseInt(Math.random(100) * 1000)}`;
let that = this;
if (this.model.state.ui.isInline) {
this.$container = $('<div id="' + this.id + '" class="datepicker-container-inline"></div>').appendTo(that.model.inputElement);
}
else {
this.$container = $('<div id="' + this.id + '" class="datepicker-container"></div>').appendTo('body');
this.hide();
this.setPickerBoxPosition();
this.addCompatibilityClass();
}
return this;
}
/**
* @desc add css class to handle compatibility ui things
*/
addCompatibilityClass () {
if (Helper.isMobile && this.model.options.responsive) {
this.$container.addClass('pwt-mobile-view');
}
}
/**
* @desc remove datepicker container element from dom
*/
destroy () {
this.$container.remove();
}
/**
* @desc set datepicker container element based on <input/> element position
*/
setPickerBoxPosition () {
let inputPosition = this.model.input.getInputPosition(),
inputSize = this.model.input.getInputSize();
if (Helper.isMobile && this.model.options.responsive) {
return false;
}
if (this.model.options.position === 'auto') {
this.$container.css({
left: (inputPosition.left) + 'px',
top: (inputSize.height + inputPosition.top) + 'px'
});
} else {
this.$container.css({
left: (this.model.options.position[1] + inputPosition.left) + 'px',
top: (this.model.options.position[0] + inputPosition.top) + 'px'
});
}
}
/**
* @desc show datepicker container element
*/
show () {
this.$container.removeClass('pwt-hide');
this.setPickerBoxPosition();
}
/**
* @desc hide datepicker container element
*/
hide () {
this.$container.addClass('pwt-hide');
}
/**
* @desc toggle datepicker container element
*/
toggle () {
this.$container.toggleClass('pwt-hide');
}
/**
* @desc return navigator switch text
* @param {String} data - accept day, month, year
* @private
* @return {String}
*/
_getNavSwitchText (data) {
let output;
if (this.model.state.viewMode == 'day') {
output = this.model.options.dayPicker.titleFormatter.call(this, data.year, data.month);
}
else if (this.model.state.viewMode == 'month') {
output = this.model.options.monthPicker.titleFormatter.call(this, data.dateObject.valueOf());
}
else if (this.model.state.viewMode == 'year') {
output = this.model.options.yearPicker.titleFormatter.call(this, data.year);
}
return output;
}
/**
* @desc check year is accessible
* @param {Number} year - year number
* @return {Boolean}
*/
checkYearAccess (year) {
let output = true;
if (this.model.state.filetredDate) {
let startYear = this.model.state.filterDate.start.year,
endYear = this.model.state.filterDate.end.year;
if (startYear && year < startYear) {
return false;
}
else if (endYear && year > endYear) {
return false;
}
}
if (output) {
return this.model.options.checkYear(year);
}
}
/**
* @private
* @param viewState
* @return {{enabled: boolean, viewMode: boolean, list: Array}}
*/
_getYearViewModel (viewState) {
let isEnabled = this.model.options.yearPicker.enabled;
// Make performance better
if (!isEnabled) {
return {
enabled: false
};
}
/**
* @description Generate years list based on viewState year
* @return ['1380',n+12,'1392']
*/
let list = [...Array(this.yearsViewCount).keys()].map(value => value + parseInt(viewState.year / this.yearsViewCount) * this.yearsViewCount);
/*
* @description Generate years object based on list
*/
let yearsModel = [],
yearStr = this.model.PersianDate.date();
for (let i of list) {
yearStr.year([i]);
yearsModel.push({
title: yearStr.format('YYYY'),
enabled: this.checkYearAccess(i),
dataYear: i,
selected: this.model.state.selected.year == i
});
}
return {
enabled: isEnabled,
viewMode: this.model.state.viewMode == 'year',
list: yearsModel
};
}
/**
* @desc check month is accessible
* @param {Number} month - month number
* @return {Boolean}
*/
checkMonthAccess (month) {
month = month + 1;
let output = true,
y = this.model.state.view.year;
if (this.model.state.filetredDate) {
let startMonth = this.model.state.filterDate.start.month,
endMonth = this.model.state.filterDate.end.month,
startYear = this.model.state.filterDate.start.year,
endYear = this.model.state.filterDate.end.year;
if (startMonth && endMonth && ((y == endYear && month > endMonth) || y > endYear) || ((y == startYear && month < startMonth) || y < startYear)) {
return false;
}
else if (endMonth && ((y == endYear && month > endMonth) || y > endYear)) {
return false;
}
else if (startMonth && ((y == startYear && month < startMonth) || y < startYear)) {
return false;
}
}
if (output) {
return this.model.options.checkMonth(month, y);
}
}
/**
* @private
* @return {{enabled: boolean, viewMode: boolean, list: Array}}
*/
_getMonthViewModel () {
let isEnaled = this.model.options.monthPicker.enabled;
// Make performance better
if (!isEnaled) {
return {
enabled: false
};
}
let monthModel = [], that = this;
for (let [index, month] of that.model.PersianDate.date().rangeName().months.entries()) {
monthModel.push({
title: month,
enabled: this.checkMonthAccess(index),
year: this.model.state.view.year,
dataMonth: index + 1,
selected: (this.model.state.selected.year == this.model.state.view.year && this.model.state.selected.month == (index + 1))
});
}
return {
enabled: isEnaled,
viewMode: this.model.state.viewMode == 'month',
list: monthModel
};
}
/**
* @desc check day is accessible
* @param {Number} thisUnix - month number
* @return {Boolean}
*/
checkDayAccess (unixtimespan) {
let self = this,
output = true;
self.minDate = this.model.options.minDate;
self.maxDate = this.model.options.maxDate;
if (self.model.state.filetredDate) {
if (self.minDate && self.maxDate) {
self.minDate = self.model.PersianDate.date(self.minDate).startOf('day').valueOf();
self.maxDate = self.model.PersianDate.date(self.maxDate).endOf('day').valueOf();
if (!(unixtimespan >= self.minDate && unixtimespan <= self.maxDate)) {
return false;
}
} else if (self.minDate) {
self.minDate = self.model.PersianDate.date(self.minDate).startOf('day').valueOf();
if (unixtimespan <= self.minDate) {
return false;
}
} else if (self.maxDate) {
self.maxDate = self.model.PersianDate.date(self.maxDate).endOf('day').valueOf();
if (unixtimespan >= self.maxDate) {
return false;
}
}
}
if (output) {
return self.model.options.checkDate(unixtimespan);
}
}
/**
* @private
* @return {object}
*/
_getDayViewModel () {
if (this.model.state.viewMode != 'day') {
return [];
}
let isEnabled = this.model.options.dayPicker.enabled;
// Make performance better
if (!isEnabled) {
return {
enabled: false
};
}
let viewMonth, viewYear;
//log('if you see this many time your code has performance issue');
viewMonth = this.model.state.view.month;
viewYear = this.model.state.view.year;
let pdateInstance = this.model.PersianDate.date(),
daysCount = pdateInstance.daysInMonth(viewYear, viewMonth),
firstWeekDayOfMonth = pdateInstance.getFirstWeekDayOfMonth(viewYear, viewMonth) - 1,
outputList = [],
daysListindex = 0,
nextMonthListIndex = 0,
daysMatrix = [
['null', 'null', 'null', 'null', 'null', 'null', 'null'],
['null', 'null', 'null', 'null', 'null', 'null', 'null'],
['null', 'null', 'null', 'null', 'null', 'null', 'null'],
['null', 'null', 'null', 'null', 'null', 'null', 'null'],
['null', 'null', 'null', 'null', 'null', 'null', 'null'],
['null', 'null', 'null', 'null', 'null', 'null', 'null']
];
const anotherCalendar = this._getAnotherCalendar();
for (let [rowIndex, daysRow] of daysMatrix.entries()) {
outputList[rowIndex] = [];
for (let [dayIndex] of daysRow.entries()) {
let calcedDate, otherMonth;
// Set hour 12 prevent issues with DST times
if (rowIndex === 0 && dayIndex < firstWeekDayOfMonth) {
calcedDate = this.model.state.view.dateObject.startOf('month').hour(12).subtract('days', ((firstWeekDayOfMonth) - dayIndex ));
otherMonth = true;
}
else if ((rowIndex === 0 && dayIndex >= firstWeekDayOfMonth) || (rowIndex <= 5 && daysListindex < daysCount)) {
daysListindex += 1;
calcedDate = new persianDate([this.model.state.view.year, this.model.state.view.month, daysListindex]);
otherMonth = false;
}
else {
nextMonthListIndex += 1;
calcedDate = this.model.state.view.dateObject.endOf('month').hour(12).add('days', nextMonthListIndex);
otherMonth = true;
}
outputList[rowIndex].push({
title: calcedDate.format('D'),
alterCalTitle: new persianDate(calcedDate.valueOf()).toCalendar(anotherCalendar[0]).toLocale(anotherCalendar[1]).format('D'),
dataDate: [calcedDate.year(), calcedDate.month(), calcedDate.date()].join(','),
dataUnix: calcedDate.hour(12).minute(0).second(0).millisecond(0).valueOf(),
otherMonth: otherMonth,
selected: this.isInSelectedDays(calcedDate.hour(12).valueOf()),
// TODO: make configurable
enabled: this.checkDayAccess(calcedDate.valueOf())
});
}
}
return {
enabled: isEnabled,
viewMode: this.model.state.viewMode == 'day',
list: outputList
};
}
markSelectedDay (unixDate = -1) {
if (this.model.options.multiSelect) {
if (unixDate !== -1){
let td = this.$container.find('td[data-unix^="'+unixDate+'"]');
if (this.isInSelectedDays(unixDate)) {
$(td[0]).removeClass('selected');
} else {
$(td[0]).addClass('selected');
}
}
} else {
const selected = this.model.state.selected;
this.$container.find('.table-days td').each(function () {
if ($(this).data('date') == [selected.year, selected.month, selected.date].join(',')) {
$(this).addClass('selected');
} else {
$(this).removeClass('selected');
}
});
}
}
isInSelectedDays (value) {
const selected = this.model.state.selectedInMultiSelectMode;
let found = false;
selected.forEach(function (obj) {
if (found) return true;
if (obj.unixDate === value) {
found = true;
}
});
return found;
}
isSelected (value) {
const selected = this.model.state.selected;
if (selected.unixDate === value)
return true;
return false;
}
markToday () {
const today = new persianDate();
this.$container.find('.table-days td').each(function () {
if ($(this).data('date') == [today.year(), today.month(), today.date()].join(',')) {
$(this).addClass('today');
} else {
$(this).removeClass('today');
}
});
}
/**
* @private
* @return {{enabled: boolean, hour: {title, enabled: boolean}, minute: {title, enabled: boolean}, second: {title, enabled: boolean}, meridian: {title: (meridian|{title, enabled}|ClassDatepicker.ClassConfig.timePicker.meridian|{enabled}|string|string), enabled: boolean}}}
*/
_getTimeViewModel () {
let isEnabled = this.model.options.timePicker.enabled;
// Make performance better
if (!isEnabled) {
return {
enabled: false
};
}
let hourTitle;
if (this.model.options.timePicker.meridian.enabled) {
hourTitle = this.model.state.view.dateObject.format('hh');
} else {
hourTitle = this.model.state.view.dateObject.format('HH');
}
return {
enabled: isEnabled,
hour: {
title: hourTitle,
enabled: this.model.options.timePicker.hour.enabled
},
minute: {
title: this.model.state.view.dateObject.format('mm'),
enabled: this.model.options.timePicker.minute.enabled
},
second: {
title: this.model.state.view.dateObject.format('ss'),
enabled: this.model.options.timePicker.second.enabled
},
meridian: {
title: this.model.state.view.dateObject.format('a'),
enabled: this.model.options.timePicker.meridian.enabled
}
};
}
/**
*
* @return {{enabled: boolean, list: (*|Array)}}
* @private
*/
_getWeekViewModel () {
let weekdaysList = [];
if (this.model.options !== null && this.model.options.customWeekdays !== undefined){
let weekdays = this.model.options.customWeekdays;
switch (weekdays.mode) {
case 'custom':
weekdaysList = weekdays.list;
break;
case 'extended':
weekdaysList = this.model.PersianDate.date().rangeName().weekdays;
break;
case 'min':
default:
weekdaysList = this.model.PersianDate.date().rangeName().weekdaysMin;
}
}else{
weekdaysList = this.model.PersianDate.date().rangeName().weekdaysMin;
}
return {
enabled: true,
list: weekdaysList
};
}
/**
*
* @return {string}
*/
getCssClass () {
return [
this.model.state.ui.isInline ? 'datepicker-plot-area-inline-view' : '',
!this.model.options.timePicker.meridian.enabled ? 'datepicker-state-no-meridian' : '',
this.model.options.onlyTimePicker ? 'datepicker-state-only-time' : '',
!this.model.options.timePicker.second.enabled ? 'datepicker-state-no-second' : '',
(this.model.options.calendar_ == 'gregorian') ? 'datepicker-gregorian' : 'datepicker-persian'
].join(' ');
}
/**
* @param data
* @return {*}
*/
getViewModel (data) {
const anotherCalendar = this._getAnotherCalendar();
return {
plotId: 'plotId' + this.model.view.id,
Id: this.model.view.id,
navigator: {
enabled: this.model.options.navigator.enabled,
switch: {
enabled: true,
text: this._getNavSwitchText(data)
},
text: this.model.options.navigator.text
},
selected: this.model.state.selected,
time: this._getTimeViewModel(data),
days: this._getDayViewModel(data),
weekdays: this._getWeekViewModel(data),
month: this._getMonthViewModel(data),
year: this._getYearViewModel(data),
toolbox: this.model.options.toolbox,
cssClass: this.getCssClass(),
theme: {
enabled: (this.model.options.theme !== undefined),
values: this.model.options.theme
},
multiSelect: ((this.model.options.multiSelect !== undefined) && (this.model.options.multiSelect === true)),
onlyTimePicker: this.model.options.onlyTimePicker,
altCalendarShowHint: this.model.options.calendar[anotherCalendar[0]].showHint,
calendarSwitchText: this.model.state.view.dateObject.toCalendar(anotherCalendar[0]).toLocale(anotherCalendar[1]).format(this.model.options.toolbox.calendarSwitch.format),
todayButtonText: this._getButtonText().todayButtontext,
submitButtonText: this._getButtonText().submitButtonText
};
}
_getButtonText () {
let output = {};
if (this.model.options.locale_ == 'fa') {
output.todayButtontext = this.model.options.toolbox.todayButton.text.fa;
output.submitButtonText = this.model.options.toolbox.submitButton.text.fa;
}
else if (this.model.options.locale_ == 'en') {
output.todayButtontext = this.model.options.toolbox.todayButton.text.en;
output.submitButtonText = this.model.options.toolbox.submitButton.text.en;
}
return output;
}
_getAnotherCalendar () {
let that = this, cal, loc;
if (that.model.options.calendar_ == 'persian') {
cal = 'gregorian';
loc = that.model.options.calendar.gregorian.locale;
}
else {
cal = 'persian';
loc = that.model.options.calendar.persian.locale;
}
return [cal, loc];
}
/**
* @desc render times area, prevent performance issue with scroll and time section
*/
renderTimePartial () {
const timeViewModel = this._getTimeViewModel(this.model.state.view);
this.$container.find('[data-time-key="hour"] input').val(timeViewModel.hour.title);
this.$container.find('[data-time-key="minute"] input').val(timeViewModel.minute.title);
this.$container.find('[data-time-key="second"] input').val(timeViewModel.second.title);
this.$container.find('[data-time-key="meridian"] input').val(timeViewModel.meridian.title);
}
/**
* @render datepicker view element
* @param data
*/
render (data) {
if (!data) {
data = this.model.state.view;
}
Helper.debug(this, 'render');
Mustache.parse(Template);
this.rendered = $(Mustache.render(this.model.options.template, this.getViewModel(data)));
this.$container.empty().append(this.rendered);
this.markSelectedDay();
this.markToday();
this.afterRender();
}
reRender () {
let data = this.model.state.view;
this.render(data);
}
/**
* @desc do after render work like attache events
*/
afterRender () {
if (this.model.navigator) {
this.model.navigator.liveAttach();
}
if (this.model.options.theme && this.model.options.theme.dayCell && this.model.options.theme.dayCell.shape === 'circle')
this.$container.find('.datepicker-plot-area .datepicker-day-view .table-days td span').css('border-radius', '50%');
}
}
module.exports = View;