335 lines
10 KiB
JavaScript
335 lines
10 KiB
JavaScript
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]();
|
|
}
|
|
}
|