class MonthRangeSlider {
options = {
onChange: null
};
constructor(containerId, startDateStr, initialStartMonth = null, initialEndMonth = null, options = null) {
this.containerId = containerId;
this.$container = $('#' + containerId);
if (options) {
this.options = { ...this.options, ...options };
}
// Парсим начальную дату
const startDateParts = startDateStr.split('-');
this.baseYear = parseInt(startDateParts[0]);
this.baseMonth = parseInt(startDateParts[1]) - 1; // 0-based
// Вычисляем общее количество месяцев от базовой даты до текущей даты + 12 месяцев
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth();
const baseDate = new Date(this.baseYear, this.baseMonth, 1);
const endDate = new Date(currentYear, currentMonth + 12, 1);
this.totalMonths = this.getMonthsDiff(baseDate, endDate);
// Инициализируем позиции слайдеров
if (initialStartMonth !== null && initialStartMonth !== undefined) {
this.startMonthIndex = initialStartMonth;
} else {
this.startMonthIndex = 0;
}
if (initialEndMonth !== null && initialEndMonth !== undefined) {
this.endMonthIndex = initialEndMonth;
} else {
this.endMonthIndex = Math.min(11, this.totalMonths - 1);
}
// Валидация
if (this.startMonthIndex < 0) this.startMonthIndex = 0;
if (this.endMonthIndex >= this.totalMonths) this.endMonthIndex = this.totalMonths - 1;
if (this.startMonthIndex > this.endMonthIndex) {
this.startMonthIndex = 0;
this.endMonthIndex = Math.min(11, this.totalMonths - 1);
}
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.initTicks();
this.initEvents();
this.updateUI();
}
getMonthsDiff(date1, date2) {
let months = (date2.getFullYear() - date1.getFullYear()) * 12;
months += date2.getMonth() - date1.getMonth();
return months + 1; // +1 чтобы включить оба месяца
}
getDateFromIndex(monthIndex) {
const totalMonths = this.baseMonth + monthIndex;
const year = this.baseYear + Math.floor(totalMonths / 12);
const month = totalMonths % 12;
return new Date(year, month, 1);
}
formatMonth(date) {
const months = [
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
];
return months[date.getMonth()] + ' ' + date.getFullYear();
}
initTicks() {
// Очищаем существующие риски
this.$ticks.empty();
// Создаем риски только для первого месяца каждого года (январь)
for (let i = 0; i < this.totalMonths; i++) {
const date = this.getDateFromIndex(i);
const isYearStart = date.getMonth() === 0; // Январь
// Создаем риску только для января
if (isYearStart) {
const percent = (i / (this.totalMonths - 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();
}
});
// Touch events для мобильных устройств
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');
// Обновляем z-index для активного слайдера
if (thumb === 'start') {
this.$thumbStart.css('z-index', '3');
this.$thumbEnd.css('z-index', '2');
} else if (thumb === 'end') {
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.startMonthIndex, this.endMonthIndex);
}
}
onDrag(e) {
const trackRect = this.$track[0].getBoundingClientRect();
const x = e.clientX - trackRect.left;
const percentage = Math.max(0, Math.min(1, x / trackRect.width));
const monthIndex = Math.round(percentage * (this.totalMonths - 1));
if (this.activeThumb === 'start') {
this.startMonthIndex = Math.min(monthIndex, this.endMonthIndex);
} else if (this.activeThumb === 'end') {
this.endMonthIndex = Math.max(monthIndex, this.startMonthIndex);
}
this.updateUI();
}
onTrackMouseDown(e) {
const trackRect = this.$track[0].getBoundingClientRect();
const x = e.clientX - trackRect.left;
const percentage = x / trackRect.width;
const monthIndex = Math.round(percentage * (this.totalMonths - 1));
// Определяем, какой ползунок ближе
const distToStart = Math.abs(monthIndex - this.startMonthIndex);
const distToEnd = Math.abs(monthIndex - this.endMonthIndex);
// Перемещаем ближайший слайдер к позиции клика и начинаем его перетаскивание
if (distToStart < distToEnd) {
this.startMonthIndex = Math.min(monthIndex, this.endMonthIndex);
this.activeThumb = 'start';
// Обновляем z-index для активного слайдера
this.$thumbStart.css('z-index', '3');
this.$thumbEnd.css('z-index', '2');
} else {
this.endMonthIndex = Math.max(monthIndex, this.startMonthIndex);
this.activeThumb = 'end';
// Обновляем z-index для активного слайдера
this.$thumbEnd.css('z-index', '3');
this.$thumbStart.css('z-index', '2');
}
this.updateUI();
// Начинаем перетаскивание выбранного слайдера
this.isDragging = true;
$('body').css('user-select', 'none');
}
updateUI() {
const startPercent = (this.startMonthIndex / (this.totalMonths - 1)) * 100;
const endPercent = (this.endMonthIndex / (this.totalMonths - 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 monthIndex = parseInt($tick.attr('data-month-index'));
if (monthIndex >= this.startMonthIndex && monthIndex <= this.endMonthIndex) {
$tick.addClass('selected');
} else {
$tick.removeClass('selected');
}
});
const startDate = this.getDateFromIndex(this.startMonthIndex);
const endDate = this.getDateFromIndex(this.endMonthIndex);
const labelText = this.formatMonth(startDate) + ' - ' + this.formatMonth(endDate);
this.$label.text(labelText);
}
getStartMonth() {
return this.startMonthIndex;
}
getEndMonth() {
return this.endMonthIndex;
}
setRange(startMonth, endMonth) {
this.startMonthIndex = Math.max(0, Math.min(startMonth, this.totalMonths - 1));
this.endMonthIndex = Math.max(0, Math.min(endMonth, this.totalMonths - 1));
if (this.startMonthIndex > this.endMonthIndex) {
const temp = this.startMonthIndex;
this.startMonthIndex = this.endMonthIndex;
this.endMonthIndex = temp;
}
this.updateUI();
}
}