second commit

This commit is contained in:
2024-12-27 22:31:23 +09:00
parent 2353324570
commit 10a0f110ca
8819 changed files with 1307198 additions and 28 deletions

334
node_modules/flowbite-datepicker/js/picker/Picker.js generated vendored Normal file
View File

@ -0,0 +1,334 @@
import {hasProperty, lastItemOf, isInRange, limitToRange} from '../lib/utils.js';
import {today} from '../lib/date.js';
import {parseHTML, showElement, hideElement, emptyChildNodes} from '../lib/dom.js';
import {registerListeners} from '../lib/event.js';
import pickerTemplate from './templates/pickerTemplate.js';
import DaysView from './views/DaysView.js';
import MonthsView from './views/MonthsView.js';
import YearsView from './views/YearsView.js';
import {triggerDatepickerEvent} from '../events/functions.js';
import {
onClickTodayBtn,
onClickClearBtn,
onClickViewSwitch,
onClickPrevBtn,
onClickNextBtn,
onClickView,
onClickPicker,
} from '../events/pickerListeners.js';
function processPickerOptions(picker, options) {
if (options.title !== undefined) {
if (options.title) {
picker.controls.title.textContent = options.title;
showElement(picker.controls.title);
} else {
picker.controls.title.textContent = '';
hideElement(picker.controls.title);
}
}
if (options.prevArrow) {
const prevBtn = picker.controls.prevBtn;
emptyChildNodes(prevBtn);
options.prevArrow.forEach((node) => {
prevBtn.appendChild(node.cloneNode(true));
});
}
if (options.nextArrow) {
const nextBtn = picker.controls.nextBtn;
emptyChildNodes(nextBtn);
options.nextArrow.forEach((node) => {
nextBtn.appendChild(node.cloneNode(true));
});
}
if (options.locale) {
picker.controls.todayBtn.textContent = options.locale.today;
picker.controls.clearBtn.textContent = options.locale.clear;
}
if (options.todayBtn !== undefined) {
if (options.todayBtn) {
showElement(picker.controls.todayBtn);
} else {
hideElement(picker.controls.todayBtn);
}
}
if (hasProperty(options, 'minDate') || hasProperty(options, 'maxDate')) {
const {minDate, maxDate} = picker.datepicker.config;
picker.controls.todayBtn.disabled = !isInRange(today(), minDate, maxDate);
}
if (options.clearBtn !== undefined) {
if (options.clearBtn) {
showElement(picker.controls.clearBtn);
} else {
hideElement(picker.controls.clearBtn);
}
}
}
// Compute view date to reset, which will be...
// - the last item of the selected dates or defaultViewDate if no selection
// - limitted to minDate or maxDate if it exceeds the range
function computeResetViewDate(datepicker) {
const {dates, config} = datepicker;
const viewDate = dates.length > 0 ? lastItemOf(dates) : config.defaultViewDate;
return limitToRange(viewDate, config.minDate, config.maxDate);
}
// Change current view's view date
function setViewDate(picker, newDate) {
const oldViewDate = new Date(picker.viewDate);
const newViewDate = new Date(newDate);
const {id, year, first, last} = picker.currentView;
const viewYear = newViewDate.getFullYear();
picker.viewDate = newDate;
if (viewYear !== oldViewDate.getFullYear()) {
triggerDatepickerEvent(picker.datepicker, 'changeYear');
}
if (newViewDate.getMonth() !== oldViewDate.getMonth()) {
triggerDatepickerEvent(picker.datepicker, 'changeMonth');
}
// return whether the new date is in different period on time from the one
// displayed in the current view
// when true, the view needs to be re-rendered on the next UI refresh.
switch (id) {
case 0:
return newDate < first || newDate > last;
case 1:
return viewYear !== year;
default:
return viewYear < first || viewYear > last;
}
}
function getTextDirection(el) {
return window.getComputedStyle(el).direction;
}
// Class representing the picker UI
export default class Picker {
constructor(datepicker) {
this.datepicker = datepicker;
const template = pickerTemplate.replace(/%buttonClass%/g, datepicker.config.buttonClass);
const element = this.element = parseHTML(template).firstChild;
const [header, main, footer] = element.firstChild.children;
const title = header.firstElementChild;
const [prevBtn, viewSwitch, nextBtn] = header.lastElementChild.children;
const [todayBtn, clearBtn] = footer.firstChild.children;
const controls = {
title,
prevBtn,
viewSwitch,
nextBtn,
todayBtn,
clearBtn,
};
this.main = main;
this.controls = controls;
const elementClass = datepicker.inline ? 'inline' : 'dropdown';
element.classList.add(`datepicker-${elementClass}`);
elementClass === 'dropdown' ? element.classList.add('dropdown', 'absolute', 'top-0', 'left-0', 'z-50', 'pt-2') : null;
processPickerOptions(this, datepicker.config);
this.viewDate = computeResetViewDate(datepicker);
// set up event listeners
registerListeners(datepicker, [
[element, 'click', onClickPicker.bind(null, datepicker), {capture: true}],
[main, 'click', onClickView.bind(null, datepicker)],
[controls.viewSwitch, 'click', onClickViewSwitch.bind(null, datepicker)],
[controls.prevBtn, 'click', onClickPrevBtn.bind(null, datepicker)],
[controls.nextBtn, 'click', onClickNextBtn.bind(null, datepicker)],
[controls.todayBtn, 'click', onClickTodayBtn.bind(null, datepicker)],
[controls.clearBtn, 'click', onClickClearBtn.bind(null, datepicker)],
]);
// set up views
this.views = [
new DaysView(this),
new MonthsView(this),
new YearsView(this, {id: 2, name: 'years', cellClass: 'year', step: 1}),
new YearsView(this, {id: 3, name: 'decades', cellClass: 'decade', step: 10}),
];
this.currentView = this.views[datepicker.config.startView];
this.currentView.render();
this.main.appendChild(this.currentView.element);
datepicker.config.container.appendChild(this.element);
}
setOptions(options) {
processPickerOptions(this, options);
this.views.forEach((view) => {
view.init(options, false);
});
this.currentView.render();
}
detach() {
this.datepicker.config.container.removeChild(this.element);
}
show() {
if (this.active) {
return;
}
this.element.classList.add('active', 'block');
this.element.classList.remove('hidden');
this.active = true;
const datepicker = this.datepicker;
if (!datepicker.inline) {
// ensure picker's direction matches input's
const inputDirection = getTextDirection(datepicker.inputField);
if (inputDirection !== getTextDirection(datepicker.config.container)) {
this.element.dir = inputDirection;
} else if (this.element.dir) {
this.element.removeAttribute('dir');
}
this.place();
if (datepicker.config.disableTouchKeyboard) {
datepicker.inputField.blur();
}
}
triggerDatepickerEvent(datepicker, 'show');
}
hide() {
if (!this.active) {
return;
}
this.datepicker.exitEditMode();
this.element.classList.remove('active', 'block');
this.element.classList.add('active', 'block', 'hidden');
this.active = false;
triggerDatepickerEvent(this.datepicker, 'hide');
}
place() {
const {classList, style} = this.element;
const {config, inputField} = this.datepicker;
const container = config.container;
const {
width: calendarWidth,
height: calendarHeight,
} = this.element.getBoundingClientRect();
const {
left: containerLeft,
top: containerTop,
width: containerWidth,
} = container.getBoundingClientRect();
const {
left: inputLeft,
top: inputTop,
width: inputWidth,
height: inputHeight
} = inputField.getBoundingClientRect();
let {x: orientX, y: orientY} = config.orientation;
let scrollTop;
let left;
let top;
if (container === document.body) {
scrollTop = window.scrollY;
left = inputLeft + window.scrollX;
top = inputTop + scrollTop;
} else {
scrollTop = container.scrollTop;
left = inputLeft - containerLeft;
top = inputTop - containerTop + scrollTop;
}
if (orientX === 'auto') {
if (left < 0) {
// align to the left and move into visible area if input's left edge < window's
orientX = 'left';
left = 10;
} else if (left + calendarWidth > containerWidth) {
// align to the right if canlendar's right edge > container's
orientX = 'right';
} else {
orientX = getTextDirection(inputField) === 'rtl' ? 'right' : 'left';
}
}
if (orientX === 'right') {
left -= calendarWidth - inputWidth;
}
if (orientY === 'auto') {
orientY = top - calendarHeight < scrollTop ? 'bottom' : 'top';
}
if (orientY === 'top') {
top -= calendarHeight;
} else {
top += inputHeight;
}
classList.remove(
'datepicker-orient-top',
'datepicker-orient-bottom',
'datepicker-orient-right',
'datepicker-orient-left'
);
classList.add(`datepicker-orient-${orientY}`, `datepicker-orient-${orientX}`);
style.top = top ? `${top}px` : top;
style.left = left ? `${left}px` : left;
}
setViewSwitchLabel(labelText) {
this.controls.viewSwitch.textContent = labelText;
}
setPrevBtnDisabled(disabled) {
this.controls.prevBtn.disabled = disabled;
}
setNextBtnDisabled(disabled) {
this.controls.nextBtn.disabled = disabled;
}
changeView(viewId) {
const oldView = this.currentView;
const newView = this.views[viewId];
if (newView.id !== oldView.id) {
this.currentView = newView;
this._renderMethod = 'render';
triggerDatepickerEvent(this.datepicker, 'changeView');
this.main.replaceChild(newView.element, oldView.element);
}
return this;
}
// Change the focused date (view date)
changeFocus(newViewDate) {
this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refreshFocus';
this.views.forEach((view) => {
view.updateFocus();
});
return this;
}
// Apply the change of the selected dates
update() {
const newViewDate = computeResetViewDate(this.datepicker);
this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refresh';
this.views.forEach((view) => {
view.updateFocus();
view.updateSelection();
});
return this;
}
// Refresh the picker UI
render(quickRender = true) {
const renderMethod = (quickRender && this._renderMethod) || 'render';
delete this._renderMethod;
this.currentView[renderMethod]();
}
}

View File

@ -0,0 +1,8 @@
import {createTagRepeat, optimizeTemplateHTML} from '../../lib/utils.js';
const calendarWeeksTemplate = optimizeTemplateHTML(`<div class="calendar-weeks">
<div class="days-of-week flex"><span class="dow h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400"></span></div>
<div class="weeks">${createTagRepeat('span', 6, {class: 'week block flex-1 leading-9 border-0 rounded-lg cursor-default text-center text-gray-900 font-semibold text-sm'})}</div>
</div>`);
export default calendarWeeksTemplate;

View File

@ -0,0 +1,8 @@
import {createTagRepeat, optimizeTemplateHTML} from '../../lib/utils.js';
const daysTemplate = optimizeTemplateHTML(`<div class="days">
<div class="days-of-week grid grid-cols-7 mb-1">${createTagRepeat('span', 7, {class: 'dow block flex-1 leading-9 border-0 rounded-lg cursor-default text-center text-gray-900 font-semibold text-sm'})}</div>
<div class="datepicker-grid w-64 grid grid-cols-7">${createTagRepeat('span', 42 , {class: 'block flex-1 leading-9 border-0 rounded-lg cursor-default text-center text-gray-900 font-semibold text-sm h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400'})}</div>
</div>`);
export default daysTemplate;

View File

@ -0,0 +1,23 @@
import {optimizeTemplateHTML} from '../../lib/utils.js';
const pickerTemplate = optimizeTemplateHTML(`<div class="datepicker hidden">
<div class="datepicker-picker inline-block rounded-lg bg-white dark:bg-gray-700 shadow-lg p-4">
<div class="datepicker-header">
<div class="datepicker-title bg-white dark:bg-gray-700 dark:text-white px-2 py-3 text-center font-semibold"></div>
<div class="datepicker-controls flex justify-between mb-2">
<button type="button" class="bg-white dark:bg-gray-700 rounded-lg text-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white text-lg p-2.5 focus:outline-none focus:ring-2 focus:ring-gray-200 prev-btn"></button>
<button type="button" class="text-sm rounded-lg text-gray-900 dark:text-white bg-white dark:bg-gray-700 font-semibold py-2.5 px-5 hover:bg-gray-100 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-200 view-switch"></button>
<button type="button" class="bg-white dark:bg-gray-700 rounded-lg text-gray-500 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white text-lg p-2.5 focus:outline-none focus:ring-2 focus:ring-gray-200 next-btn"></button>
</div>
</div>
<div class="datepicker-main p-1"></div>
<div class="datepicker-footer">
<div class="datepicker-controls flex space-x-2 rtl:space-x-reverse mt-2">
<button type="button" class="%buttonClass% today-btn text-white bg-blue-700 !bg-primary-700 dark:bg-blue-600 dark:!bg-primary-600 hover:bg-blue-800 hover:!bg-primary-800 dark:hover:bg-blue-700 dark:hover:!bg-primary-700 focus:ring-4 focus:ring-blue-300 focus:!ring-primary-300 font-medium rounded-lg text-sm px-5 py-2 text-center w-1/2"></button>
<button type="button" class="%buttonClass% clear-btn text-gray-900 dark:text-white bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600 focus:ring-4 focus:ring-blue-300 focus:!ring-primary-300 font-medium rounded-lg text-sm px-5 py-2 text-center w-1/2"></button>
</div>
</div>
</div>
</div>`);
export default pickerTemplate;

View File

@ -0,0 +1,250 @@
import {hasProperty, pushUnique} from '../../lib/utils.js';
import {today, dateValue, addDays, addWeeks, dayOfTheWeekOf, getWeek} from '../../lib/date.js';
import {formatDate} from '../../lib/date-format.js';
import {parseHTML, showElement, hideElement} from '../../lib/dom.js';
import daysTemplate from '../templates/daysTemplate.js';
import calendarWeeksTemplate from '../templates/calendarWeeksTemplate.js';
import View from './View.js';
export default class DaysView extends View {
constructor(picker) {
super(picker, {
id: 0,
name: 'days',
cellClass: 'day',
});
}
init(options, onConstruction = true) {
if (onConstruction) {
const inner = parseHTML(daysTemplate).firstChild;
this.dow = inner.firstChild;
this.grid = inner.lastChild;
this.element.appendChild(inner);
}
super.init(options);
}
setOptions(options) {
let updateDOW;
if (hasProperty(options, 'minDate')) {
this.minDate = options.minDate;
}
if (hasProperty(options, 'maxDate')) {
this.maxDate = options.maxDate;
}
if (options.datesDisabled) {
this.datesDisabled = options.datesDisabled;
}
if (options.daysOfWeekDisabled) {
this.daysOfWeekDisabled = options.daysOfWeekDisabled;
updateDOW = true;
}
if (options.daysOfWeekHighlighted) {
this.daysOfWeekHighlighted = options.daysOfWeekHighlighted;
}
if (options.todayHighlight !== undefined) {
this.todayHighlight = options.todayHighlight;
}
if (options.weekStart !== undefined) {
this.weekStart = options.weekStart;
this.weekEnd = options.weekEnd;
updateDOW = true;
}
if (options.locale) {
const locale = this.locale = options.locale;
this.dayNames = locale.daysMin;
this.switchLabelFormat = locale.titleFormat;
updateDOW = true;
}
if (options.beforeShowDay !== undefined) {
this.beforeShow = typeof options.beforeShowDay === 'function'
? options.beforeShowDay
: undefined;
}
if (options.calendarWeeks !== undefined) {
if (options.calendarWeeks && !this.calendarWeeks) {
const weeksElem = parseHTML(calendarWeeksTemplate).firstChild;
this.calendarWeeks = {
element: weeksElem,
dow: weeksElem.firstChild,
weeks: weeksElem.lastChild,
};
this.element.insertBefore(weeksElem, this.element.firstChild);
} else if (this.calendarWeeks && !options.calendarWeeks) {
this.element.removeChild(this.calendarWeeks.element);
this.calendarWeeks = null;
}
}
if (options.showDaysOfWeek !== undefined) {
if (options.showDaysOfWeek) {
showElement(this.dow);
if (this.calendarWeeks) {
showElement(this.calendarWeeks.dow);
}
} else {
hideElement(this.dow);
if (this.calendarWeeks) {
hideElement(this.calendarWeeks.dow);
}
}
}
// update days-of-week when locale, daysOfweekDisabled or weekStart is changed
if (updateDOW) {
Array.from(this.dow.children).forEach((el, index) => {
const dow = (this.weekStart + index) % 7;
el.textContent = this.dayNames[dow];
el.className = this.daysOfWeekDisabled.includes(dow) ? 'dow disabled text-center h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400 cursor-not-allowed' : 'dow text-center h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400';
});
}
}
// Apply update on the focused date to view's settings
updateFocus() {
const viewDate = new Date(this.picker.viewDate);
const viewYear = viewDate.getFullYear();
const viewMonth = viewDate.getMonth();
const firstOfMonth = dateValue(viewYear, viewMonth, 1);
const start = dayOfTheWeekOf(firstOfMonth, this.weekStart, this.weekStart);
this.first = firstOfMonth;
this.last = dateValue(viewYear, viewMonth + 1, 0);
this.start = start;
this.focused = this.picker.viewDate;
}
// Apply update on the selected dates to view's settings
updateSelection() {
const {dates, rangepicker} = this.picker.datepicker;
this.selected = dates;
if (rangepicker) {
this.range = rangepicker.dates;
}
}
// Update the entire view UI
render() {
// update today marker on ever render
this.today = this.todayHighlight ? today() : undefined;
// refresh disabled dates on every render in order to clear the ones added
// by beforeShow hook at previous render
this.disabled = [...this.datesDisabled];
const switchLabel = formatDate(this.focused, this.switchLabelFormat, this.locale);
this.picker.setViewSwitchLabel(switchLabel);
this.picker.setPrevBtnDisabled(this.first <= this.minDate);
this.picker.setNextBtnDisabled(this.last >= this.maxDate);
if (this.calendarWeeks) {
// start of the UTC week (Monday) of the 1st of the month
const startOfWeek = dayOfTheWeekOf(this.first, 1, 1);
Array.from(this.calendarWeeks.weeks.children).forEach((el, index) => {
el.textContent = getWeek(addWeeks(startOfWeek, index));
});
}
Array.from(this.grid.children).forEach((el, index) => {
const classList = el.classList;
const current = addDays(this.start, index);
const date = new Date(current);
const day = date.getDay();
el.className = `datepicker-cell hover:bg-gray-100 dark:hover:bg-gray-600 block flex-1 leading-9 border-0 rounded-lg cursor-pointer text-center text-gray-900 dark:text-white font-semibold text-sm ${this.cellClass}`;
el.dataset.date = current;
el.textContent = date.getDate();
if (current < this.first) {
classList.add('prev', 'text-gray-500', 'dark:text-white');
} else if (current > this.last) {
classList.add('next', 'text-gray-500', 'dark:text-white');
}
if (this.today === current) {
classList.add('today', 'bg-gray-100', 'dark:bg-gray-600');
}
if (current < this.minDate || current > this.maxDate || this.disabled.includes(current)) {
classList.add('disabled', 'cursor-not-allowed', 'text-gray-400', 'dark:text-gray-500');
classList.remove('hover:bg-gray-100', 'dark:hover:bg-gray-600', 'text-gray-900', 'dark:text-white', 'cursor-pointer');
}
if (this.daysOfWeekDisabled.includes(day)) {
classList.add('disabled', 'cursor-not-allowed', 'text-gray-400', 'dark:text-gray-500');
classList.remove('hover:bg-gray-100', 'dark:hover:bg-gray-600', 'text-gray-900', 'dark:text-white', 'cursor-pointer');
pushUnique(this.disabled, current);
}
if (this.daysOfWeekHighlighted.includes(day)) {
classList.add('highlighted');
}
if (this.range) {
const [rangeStart, rangeEnd] = this.range;
if (current > rangeStart && current < rangeEnd) {
classList.add('range', 'bg-gray-200', 'dark:bg-gray-600');
classList.remove('rounded-lg', 'rounded-l-lg', 'rounded-r-lg')
}
if (current === rangeStart) {
classList.add('range-start', 'bg-gray-100', 'dark:bg-gray-600', 'rounded-l-lg');
classList.remove('rounded-lg', 'rounded-r-lg');
}
if (current === rangeEnd) {
classList.add('range-end', 'bg-gray-100', 'dark:bg-gray-600', 'rounded-r-lg');
classList.remove('rounded-lg', 'rounded-l-lg');
}
}
if (this.selected.includes(current)) {
classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
classList.remove('text-gray-900', 'text-gray-500', 'hover:bg-gray-100', 'dark:text-white', 'dark:hover:bg-gray-600', 'dark:bg-gray-600', 'bg-gray-100', 'bg-gray-200');
}
if (current === this.focused) {
classList.add('focused');
}
if (this.beforeShow) {
this.performBeforeHook(el, current, current);
}
});
}
// Update the view UI by applying the changes of selected and focused items
refresh() {
const [rangeStart, rangeEnd] = this.range || [];
this.grid
.querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
.forEach((el) => {
el.classList.remove('range', 'range-start', 'range-end', 'selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white', 'focused');
el.classList.add('text-gray-900', 'rounded-lg', 'dark:text-white');
});
Array.from(this.grid.children).forEach((el) => {
const current = Number(el.dataset.date);
const classList = el.classList;
classList.remove('bg-gray-200', 'dark:bg-gray-600', 'rounded-l-lg', 'rounded-r-lg')
if (current > rangeStart && current < rangeEnd) {
classList.add('range', 'bg-gray-200', 'dark:bg-gray-600');
classList.remove('rounded-lg');
}
if (current === rangeStart) {
classList.add('range-start', 'bg-gray-200', 'dark:bg-gray-600', 'rounded-l-lg');
classList.remove('rounded-lg',);
}
if (current === rangeEnd) {
classList.add('range-end', 'bg-gray-200', 'dark:bg-gray-600', 'rounded-r-lg');
classList.remove('rounded-lg',);
}
if (this.selected.includes(current)) {
classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
classList.remove('text-gray-900', 'hover:bg-gray-100', 'dark:text-white', 'dark:hover:bg-gray-600', 'bg-gray-100', 'bg-gray-200', 'dark:bg-gray-600');
}
if (current === this.focused) {
classList.add('focused');
}
});
}
// Update the view UI by applying the change of focused item
refreshFocus() {
const index = Math.round((this.focused - this.start) / 86400000);
this.grid.querySelectorAll('.focused').forEach((el) => {
el.classList.remove('focused');
});
this.grid.children[index].classList.add('focused');
}
}

View File

@ -0,0 +1,198 @@
import {hasProperty, pushUnique, createTagRepeat} from '../../lib/utils.js';
import {dateValue} from '../../lib/date.js';
import {parseHTML} from '../../lib/dom.js';
import View from './View.js';
function computeMonthRange(range, thisYear) {
if (!range || !range[0] || !range[1]) {
return;
}
const [[startY, startM], [endY, endM]] = range;
if (startY > thisYear || endY < thisYear) {
return;
}
return [
startY === thisYear ? startM : -1,
endY === thisYear ? endM : 12,
];
}
export default class MonthsView extends View {
constructor(picker) {
super(picker, {
id: 1,
name: 'months',
cellClass: 'month',
});
}
init(options, onConstruction = true) {
if (onConstruction) {
this.grid = this.element;
this.element.classList.add('months', 'datepicker-grid', 'w-64', 'grid', 'grid-cols-4');
this.grid.appendChild(parseHTML(createTagRepeat('span', 12, {'data-month': ix => ix})));
}
super.init(options);
}
setOptions(options) {
if (options.locale) {
this.monthNames = options.locale.monthsShort;
}
if (hasProperty(options, 'minDate')) {
if (options.minDate === undefined) {
this.minYear = this.minMonth = this.minDate = undefined;
} else {
const minDateObj = new Date(options.minDate);
this.minYear = minDateObj.getFullYear();
this.minMonth = minDateObj.getMonth();
this.minDate = minDateObj.setDate(1);
}
}
if (hasProperty(options, 'maxDate')) {
if (options.maxDate === undefined) {
this.maxYear = this.maxMonth = this.maxDate = undefined;
} else {
const maxDateObj = new Date(options.maxDate);
this.maxYear = maxDateObj.getFullYear();
this.maxMonth = maxDateObj.getMonth();
this.maxDate = dateValue(this.maxYear, this.maxMonth + 1, 0);
}
}
if (options.beforeShowMonth !== undefined) {
this.beforeShow = typeof options.beforeShowMonth === 'function'
? options.beforeShowMonth
: undefined;
}
}
// Update view's settings to reflect the viewDate set on the picker
updateFocus() {
const viewDate = new Date(this.picker.viewDate);
this.year = viewDate.getFullYear();
this.focused = viewDate.getMonth();
}
// Update view's settings to reflect the selected dates
updateSelection() {
const {dates, rangepicker} = this.picker.datepicker;
this.selected = dates.reduce((selected, timeValue) => {
const date = new Date(timeValue);
const year = date.getFullYear();
const month = date.getMonth();
if (selected[year] === undefined) {
selected[year] = [month];
} else {
pushUnique(selected[year], month);
}
return selected;
}, {});
if (rangepicker && rangepicker.dates) {
this.range = rangepicker.dates.map(timeValue => {
const date = new Date(timeValue);
return isNaN(date) ? undefined : [date.getFullYear(), date.getMonth()];
});
}
}
// Update the entire view UI
render() {
// refresh disabled months on every render in order to clear the ones added
// by beforeShow hook at previous render
this.disabled = [];
this.picker.setViewSwitchLabel(this.year);
this.picker.setPrevBtnDisabled(this.year <= this.minYear);
this.picker.setNextBtnDisabled(this.year >= this.maxYear);
const selected = this.selected[this.year] || [];
const yrOutOfRange = this.year < this.minYear || this.year > this.maxYear;
const isMinYear = this.year === this.minYear;
const isMaxYear = this.year === this.maxYear;
const range = computeMonthRange(this.range, this.year);
Array.from(this.grid.children).forEach((el, index) => {
const classList = el.classList;
const date = dateValue(this.year, index, 1);
el.className = `datepicker-cell hover:bg-gray-100 dark:hover:bg-gray-600 block flex-1 leading-9 border-0 rounded-lg cursor-pointer text-center text-gray-900 dark:text-white font-semibold text-sm ${this.cellClass}`;
if (this.isMinView) {
el.dataset.date = date;
}
// reset text on every render to clear the custom content set
// by beforeShow hook at previous render
el.textContent = this.monthNames[index];
if (
yrOutOfRange
|| isMinYear && index < this.minMonth
|| isMaxYear && index > this.maxMonth
) {
classList.add('disabled');
}
if (range) {
const [rangeStart, rangeEnd] = range;
if (index > rangeStart && index < rangeEnd) {
classList.add('range');
}
if (index === rangeStart) {
classList.add('range-start');
}
if (index === rangeEnd) {
classList.add('range-end');
}
}
if (selected.includes(index)) {
classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
classList.remove('text-gray-900', 'hover:bg-gray-100', 'dark:text-white', 'dark:hover:bg-gray-600');
}
if (index === this.focused) {
classList.add('focused');
}
if (this.beforeShow) {
this.performBeforeHook(el, index, date);
}
});
}
// Update the view UI by applying the changes of selected and focused items
refresh() {
const selected = this.selected[this.year] || [];
const [rangeStart, rangeEnd] = computeMonthRange(this.range, this.year) || [];
this.grid
.querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
.forEach((el) => {
el.classList.remove('range', 'range-start', 'range-end', 'selected', 'bg-blue-700', '!bg-primary-700', 'dark:bg-blue-600', 'dark:!bg-primary-700', 'dark:text-white', 'text-white', 'focused');
el.classList.add('text-gray-900', 'hover:bg-gray-100', 'dark:text-white', 'dark:hover:bg-gray-600');
});
Array.from(this.grid.children).forEach((el, index) => {
const classList = el.classList;
if (index > rangeStart && index < rangeEnd) {
classList.add('range');
}
if (index === rangeStart) {
classList.add('range-start');
}
if (index === rangeEnd) {
classList.add('range-end');
}
if (selected.includes(index)) {
classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
classList.remove('text-gray-900', 'hover:bg-gray-100', 'dark:text-white', 'dark:hover:bg-gray-600');
}
if (index === this.focused) {
classList.add('focused');
}
});
}
// Update the view UI by applying the change of focused item
refreshFocus() {
this.grid.querySelectorAll('.focused').forEach((el) => {
el.classList.remove('focused');
});
this.grid.children[this.focused].classList.add('focused');
}
}

View File

@ -0,0 +1,55 @@
import {pushUnique} from '../../lib/utils.js';
import {parseHTML, replaceChildNodes} from '../../lib/dom.js';
// Base class of the view classes
export default class View {
constructor(picker, config) {
Object.assign(this, config, {
picker,
element: parseHTML(`<div class="datepicker-view flex"></div>`).firstChild,
selected: [],
});
this.init(this.picker.datepicker.config);
}
init(options) {
if (options.pickLevel !== undefined) {
this.isMinView = this.id === options.pickLevel;
}
this.setOptions(options);
this.updateFocus();
this.updateSelection();
}
// Execute beforeShow() callback and apply the result to the element
// args:
// - current - current value on the iteration on view rendering
// - timeValue - time value of the date to pass to beforeShow()
performBeforeHook(el, current, timeValue) {
let result = this.beforeShow(new Date(timeValue));
switch (typeof result) {
case 'boolean':
result = {enabled: result};
break;
case 'string':
result = {classes: result};
}
if (result) {
if (result.enabled === false) {
el.classList.add('disabled');
pushUnique(this.disabled, current);
}
if (result.classes) {
const extraClasses = result.classes.split(/\s+/);
el.classList.add(...extraClasses);
if (extraClasses.includes('disabled')) {
pushUnique(this.disabled, current);
}
}
if (result.content) {
replaceChildNodes(el, result.content);
}
}
}
}

View File

@ -0,0 +1,170 @@
import {hasProperty, pushUnique, createTagRepeat} from '../../lib/utils.js';
import {dateValue, startOfYearPeriod} from '../../lib/date.js';
import {parseHTML} from '../../lib/dom.js';
import View from './View.js';
function toTitleCase(word) {
return [...word].reduce((str, ch, ix) => str += ix ? ch : ch.toUpperCase(), '');
}
// Class representing the years and decades view elements
export default class YearsView extends View {
constructor(picker, config) {
super(picker, config);
}
init(options, onConstruction = true) {
if (onConstruction) {
this.navStep = this.step * 10;
this.beforeShowOption = `beforeShow${toTitleCase(this.cellClass)}`;
this.grid = this.element;
this.element.classList.add(this.name, 'datepicker-grid', 'w-64', 'grid', 'grid-cols-4');
this.grid.appendChild(parseHTML(createTagRepeat('span', 12)));
}
super.init(options);
}
setOptions(options) {
if (hasProperty(options, 'minDate')) {
if (options.minDate === undefined) {
this.minYear = this.minDate = undefined;
} else {
this.minYear = startOfYearPeriod(options.minDate, this.step);
this.minDate = dateValue(this.minYear, 0, 1);
}
}
if (hasProperty(options, 'maxDate')) {
if (options.maxDate === undefined) {
this.maxYear = this.maxDate = undefined;
} else {
this.maxYear = startOfYearPeriod(options.maxDate, this.step);
this.maxDate = dateValue(this.maxYear, 11, 31);
}
}
if (options[this.beforeShowOption] !== undefined) {
const beforeShow = options[this.beforeShowOption];
this.beforeShow = typeof beforeShow === 'function' ? beforeShow : undefined;
}
}
// Update view's settings to reflect the viewDate set on the picker
updateFocus() {
const viewDate = new Date(this.picker.viewDate);
const first = startOfYearPeriod(viewDate, this.navStep);
const last = first + 9 * this.step;
this.first = first;
this.last = last;
this.start = first - this.step;
this.focused = startOfYearPeriod(viewDate, this.step);
}
// Update view's settings to reflect the selected dates
updateSelection() {
const {dates, rangepicker} = this.picker.datepicker;
this.selected = dates.reduce((years, timeValue) => {
return pushUnique(years, startOfYearPeriod(timeValue, this.step));
}, []);
if (rangepicker && rangepicker.dates) {
this.range = rangepicker.dates.map(timeValue => {
if (timeValue !== undefined) {
return startOfYearPeriod(timeValue, this.step);
}
});
}
}
// Update the entire view UI
render() {
// refresh disabled years on every render in order to clear the ones added
// by beforeShow hook at previous render
this.disabled = [];
this.picker.setViewSwitchLabel(`${this.first}-${this.last}`);
this.picker.setPrevBtnDisabled(this.first <= this.minYear);
this.picker.setNextBtnDisabled(this.last >= this.maxYear);
Array.from(this.grid.children).forEach((el, index) => {
const classList = el.classList;
const current = this.start + (index * this.step);
const date = dateValue(current, 0, 1);
el.className = `datepicker-cell hover:bg-gray-100 dark:hover:bg-gray-600 block flex-1 leading-9 border-0 rounded-lg cursor-pointer text-center text-gray-900 dark:text-white font-semibold text-sm ${this.cellClass}`;
if (this.isMinView) {
el.dataset.date = date;
}
el.textContent = el.dataset.year = current;
if (index === 0) {
classList.add('prev');
} else if (index === 11) {
classList.add('next');
}
if (current < this.minYear || current > this.maxYear) {
classList.add('disabled');
}
if (this.range) {
const [rangeStart, rangeEnd] = this.range;
if (current > rangeStart && current < rangeEnd) {
classList.add('range');
}
if (current === rangeStart) {
classList.add('range-start');
}
if (current === rangeEnd) {
classList.add('range-end');
}
}
if (this.selected.includes(current)) {
classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
classList.remove('text-gray-900', 'hover:bg-gray-100', 'dark:text-white', 'dark:hover:bg-gray-600');
}
if (current === this.focused) {
classList.add('focused');
}
if (this.beforeShow) {
this.performBeforeHook(el, current, date);
}
});
}
// Update the view UI by applying the changes of selected and focused items
refresh() {
const [rangeStart, rangeEnd] = this.range || [];
this.grid
.querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
.forEach((el) => {
el.classList.remove('range', 'range-start', 'range-end', 'selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark!bg-primary-600', 'dark:text-white', 'focused');
});
Array.from(this.grid.children).forEach((el) => {
const current = Number(el.textContent);
const classList = el.classList;
if (current > rangeStart && current < rangeEnd) {
classList.add('range');
}
if (current === rangeStart) {
classList.add('range-start');
}
if (current === rangeEnd) {
classList.add('range-end');
}
if (this.selected.includes(current)) {
classList.add('selected', 'bg-blue-700', '!bg-primary-700', 'text-white', 'dark:bg-blue-600', 'dark:!bg-primary-600', 'dark:text-white');
classList.remove('text-gray-900', 'hover:bg-gray-100', 'dark:text-white', 'dark:hover:bg-gray-600');
}
if (current === this.focused) {
classList.add('focused');
}
});
}
// Update the view UI by applying the change of focused item
refreshFocus() {
const index = Math.round((this.focused - this.start) / this.step);
this.grid.querySelectorAll('.focused').forEach((el) => {
el.classList.remove('focused');
});
this.grid.children[index].classList.add('focused');
}
}