class MonthRangeSlider {
options = {
onChange: null,
startDate: null,
endDate: null,
};
containerId = null;
$container = null;
/** Левая граница шкалы */
minDate = null;
/** Правая граница шкалы */
maxDate = null;
/** Текущий выбранный старт */
startDate = null;
/** Текущий выбранный конец */
endDate = null;
isDragging = false;
activeThumb = null;
$trackContainer = null;
$track = null;
$selected = null;
$thumbStart = null;
$thumbEnd = null;
$label = null;
$ticks = null;
$innerTwoColorThumbStart = null;
$innerTwoColorThumbEnd = null;
_innerHideTimeoutStart = null;
_innerHideTimeoutEnd = null;
constructor(containerId, minDate, maxDate = null, options = null) {
this.containerId = containerId;
this.$container = $('#' + containerId);
if (options) {
this.options = { ...this.options, ...options };
}
this.minDate = new Date(minDate.getFullYear(), minDate.getMonth(), 1);
if (maxDate instanceof Date) {
this.maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), 1);
} else {
const now = new Date();
this.maxDate = new Date(now.getFullYear(), now.getMonth() + 12, 1);
}
const startDate = this.options.startDate instanceof Date ? this.options.startDate : null;
const endDate = this.options.endDate instanceof Date ? this.options.endDate : null;
this.startDate = startDate
? new Date(startDate.getFullYear(), startDate.getMonth(), 1)
: new Date(this.minDate);
this.endDate = endDate
? new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0)
: new Date(this.minDate.getFullYear(), this.minDate.getMonth() + Math.min(11, this._totalMonths() - 1) + 1, 0);
this._clampDates();
this.isDragging = false;
this.activeThumb = null;
this.$trackContainer = this.$container.find('.month-range-track-container');
this.$track = this.$container.find('.month-range-track');
this.$selected = this.$container.find('.month-range-selected');
this.$thumbStart = this.$container.find('.month-range-thumb-start');
this.$thumbEnd = this.$container.find('.month-range-thumb-end');
this.$label = this.$container.find('.month-range-label');
this.$ticks = this.$container.find('.month-range-ticks');
this.$innerTwoColorThumbStart = this.$thumbStart.find('.month-range-inner-outer');
this.$innerTwoColorThumbEnd = this.$thumbEnd.find('.month-range-inner-outer');
this.initTicks();
this.initEvents();
this.updateUI();
}
_totalMonths() {
return (this.maxDate.getFullYear() - this.minDate.getFullYear()) * 12
+ (this.maxDate.getMonth() - this.minDate.getMonth()) + 1;
}
_clampDates() {
if (this.startDate < this.minDate) this.startDate = new Date(this.minDate);
if (this.endDate > this.maxDate) this.endDate = new Date(this.maxDate);
if (this.startDate > this.endDate) {
this.startDate = new Date(this.minDate);
this.endDate = new Date(this.minDate.getFullYear(), this.minDate.getMonth() + Math.min(11, this._totalMonths() - 1) + 1, 0);
}
}
_dateToIndex(date) {
return (date.getFullYear() - this.minDate.getFullYear()) * 12
+ (date.getMonth() - this.minDate.getMonth());
}
_indexToDate(index) {
return new Date(this.minDate.getFullYear(), this.minDate.getMonth() + index, 1);
}
formatMonth(date) {
const months = [
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
];
return months[date.getMonth()] + ' ' + date.getFullYear();
}
initTicks() {
this.$ticks.empty();
const total = this._totalMonths();
for (let i = 0; i < total; i++) {
const date = this._indexToDate(i);
if (date.getMonth() !== 0) continue;
const percent = (i / (total - 1)) * 100;
const $tick = $('<div></div>')
.addClass('month-range-tick major')
.attr('data-month-index', i)
.css('left', percent + '%');
const $label = $('<div></div>')
.addClass('month-range-tick-label')
.text(date.getFullYear());
$tick.append($label);
this.$ticks.append($tick);
}
}
initEvents() {
const self = this;
this.$thumbStart.on('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
self.startDragging('start');
});
this.$thumbEnd.on('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
self.startDragging('end');
});
this.$trackContainer.on('mousedown', function(e) {
e.preventDefault();
self.onTrackMouseDown(e);
});
$(document).on('mousemove', function(e) {
if (self.isDragging) {
self.onDrag(e);
}
});
$(document).on('mouseup', function() {
if (self.isDragging) {
self.stopDragging();
}
});
this.$thumbStart.on('touchstart', function(e) {
e.preventDefault();
e.stopPropagation();
self.startDragging('start');
});
this.$thumbEnd.on('touchstart', function(e) {
e.preventDefault();
e.stopPropagation();
self.startDragging('end');
});
this.$trackContainer.on('touchstart', function(e) {
e.preventDefault();
self.onTrackMouseDown(e.touches[0]);
});
$(document).on('touchmove', function(e) {
if (self.isDragging) {
self.onDrag(e.touches ? e.touches[0] : e);
}
});
$(document).on('touchend', function() {
if (self.isDragging) {
self.stopDragging();
}
});
}
startDragging(thumb) {
this.isDragging = true;
this.activeThumb = thumb;
$('body').css('user-select', 'none');
if (thumb === 'start') {
this.$thumbStart.css('z-index', '3');
this.$thumbEnd.css('z-index', '2');
} else {
this.$thumbEnd.css('z-index', '3');
this.$thumbStart.css('z-index', '2');
}
}
stopDragging() {
this.isDragging = false;
this.activeThumb = null;
$('body').css('user-select', '');
if (this.options.onChange) {
this.options.onChange(this.startDate, this.endDate);
}
}
_indexFromEvent(e) {
const trackRect = this.$track[0].getBoundingClientRect();
const x = e.clientX - trackRect.left;
const percentage = Math.max(0, Math.min(1, x / trackRect.width));
return Math.round(percentage * (this._totalMonths() - 1));
}
onDrag(e) {
const index = this._indexFromEvent(e);
if (this.activeThumb === 'start') {
const newStart = this._indexToDate(index);
this.startDate = newStart;
if (this.startDate > this.endDate) {
this.endDate = new Date(this.startDate);
}
} else if (this.activeThumb === 'end') {
const d = this._indexToDate(index);
this.endDate = new Date(d.getFullYear(), d.getMonth() + 1, 0);
if (this.endDate < this.startDate) {
this.startDate = new Date(this.endDate);
}
}
this.updateUI();
}
onTrackMouseDown(e) {
const index = this._indexFromEvent(e);
const startIndex = this._dateToIndex(this.startDate);
const endIndex = this._dateToIndex(this.endDate);
const distToStart = Math.abs(index - startIndex);
const distToEnd = Math.abs(index - endIndex);
if (distToStart < distToEnd) {
this.startDate = this._indexToDate(Math.min(index, endIndex));
this.activeThumb = 'start';
this.$thumbStart.css('z-index', '3');
this.$thumbEnd.css('z-index', '2');
} else {
const d = this._indexToDate(Math.max(index, startIndex));
this.endDate = new Date(d.getFullYear(), d.getMonth() + 1, 0);
this.activeThumb = 'end';
this.$thumbEnd.css('z-index', '3');
this.$thumbStart.css('z-index', '2');
}
this.updateUI();
this.isDragging = true;
$('body').css('user-select', 'none');
}
updateUI() {
const total = this._totalMonths();
const startIndex = this._dateToIndex(this.startDate);
const endIndex = this._dateToIndex(this.endDate);
const startPercent = (startIndex / (total - 1)) * 100;
const endPercent = (endIndex / (total - 1)) * 100;
this.$thumbStart.css('left', startPercent + '%');
this.$thumbEnd.css('left', endPercent + '%');
this.$selected.css({
'left': startPercent + '%',
'width': (endPercent - startPercent) + '%'
});
this.$ticks.find('.month-range-tick').each((index, tick) => {
const $tick = $(tick);
const i = parseInt($tick.attr('data-month-index'));
if (i >= startIndex && i <= endIndex) {
$tick.addClass('selected');
} else {
$tick.removeClass('selected');
}
});
this.$label.text(this.formatMonth(this.startDate) + ' - ' + this.formatMonth(this.endDate));
if (startIndex === endIndex) {
if (this.$innerTwoColorThumbStart) {
clearTimeout(this._innerHideTimeoutStart);
this.$innerTwoColorThumbStart.css('display', 'block');
setTimeout(() => this.$innerTwoColorThumbStart.css('opacity', 1), 10);
}
if (this.$innerTwoColorThumbEnd) {
clearTimeout(this._innerHideTimeoutEnd);
this.$innerTwoColorThumbEnd.css('display', 'block');
setTimeout(() => this.$innerTwoColorThumbEnd.css('opacity', 1), 10);
}
} else {
if (this.$innerTwoColorThumbStart) {
this.$innerTwoColorThumbStart.css('opacity', 0);
clearTimeout(this._innerHideTimeoutStart);
this._innerHideTimeoutStart = setTimeout(() => {
if (this.$innerTwoColorThumbStart) this.$innerTwoColorThumbStart.css('display', 'none');
}, 180);
}
if (this.$innerTwoColorThumbEnd) {
this.$innerTwoColorThumbEnd.css('opacity', 0);
clearTimeout(this._innerHideTimeoutEnd);
this._innerHideTimeoutEnd = setTimeout(() => {
if (this.$innerTwoColorThumbEnd) this.$innerTwoColorThumbEnd.css('display', 'none');
}, 180);
}
}
}
getStartDate() {
return this.startDate;
}
getEndDate() {
return this.endDate;
}
setRange(startDate, endDate) {
this.startDate = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
this.endDate = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0);
if (this.startDate > this.endDate) {
const tmp = this.startDate;
this.startDate = this.endDate;
this.endDate = tmp;
}
this._clampDates();
this.updateUI();
}
}