vanillajs-datepicker
Version:
A vanilla JavaScript remake of bootstrap-datepicker for Bulma and other CSS frameworks
358 lines (333 loc) • 10.9 kB
JavaScript
import {pushUnique} from '../lib/utils.js';
import {
dateValue,
regularizeDate,
getIsoWeek,
getWesternTradWeek,
getMidEasternWeek,
} from '../lib/date.js';
import {reFormatTokens, parseDate} from '../lib/date-format.js';
import {parseHTML} from '../lib/dom.js';
import defaultOptions from './defaultOptions.js';
const {
language: defaultLang,
format: defaultFormat,
weekStart: defaultWeekStart,
} = defaultOptions;
// Reducer function to filter out invalid day-of-week from the input
function sanitizeDOW(dow, day) {
return dow.length < 6 && day >= 0 && day < 7
? pushUnique(dow, day)
: dow;
}
function determineGetWeekMethod(numberingMode, weekStart) {
const methodId = numberingMode === 4
? (weekStart === 6 ? 3 : !weekStart + 1)
: numberingMode;
switch (methodId) {
case 1:
return getIsoWeek;
case 2:
return getWesternTradWeek;
case 3:
return getMidEasternWeek;
}
}
function updateWeekStart(newValue, config, weekNumbers) {
config.weekStart = newValue;
config.weekEnd = (newValue + 6) % 7;
if (weekNumbers === 4) {
config.getWeekNumber = determineGetWeekMethod(4, newValue);
}
return newValue;
}
// validate input date. if invalid, fallback to the original value
function validateDate(value, format, locale, origValue) {
const date = parseDate(value, format, locale);
return date !== undefined ? date : origValue;
}
// Validate viewId. if invalid, fallback to the original value
function validateViewId(value, origValue, max = 3) {
const viewId = parseInt(value, 10);
return viewId >= 0 && viewId <= max ? viewId : origValue;
}
function replaceOptions(options, from, to, convert = undefined) {
if (from in options) {
if (!(to in options)) {
options[to] = convert ? convert(options[from]) : options[from];
}
delete options[from];
}
}
// Create Datepicker configuration to set
export default function processOptions(options, datepicker) {
const inOpts = Object.assign({}, options);
const config = {};
const locales = datepicker.constructor.locales;
const rangeEnd = !!datepicker.rangeSideIndex;
let {
datesDisabled,
format,
language,
locale,
maxDate,
maxView,
minDate,
pickLevel,
startView,
weekNumbers,
weekStart,
} = datepicker.config || {};
// for backword compatibility
replaceOptions(inOpts, 'calendarWeeks', 'weekNumbers', val => val ? 1 : 0);
replaceOptions(inOpts, 'clearBtn', 'clearButton');
replaceOptions(inOpts, 'todayBtn', 'todayButton');
replaceOptions(inOpts, 'todayBtnMode', 'todayButtonMode');
if (inOpts.language) {
let lang;
if (inOpts.language !== language) {
if (locales[inOpts.language]) {
lang = inOpts.language;
} else {
// Check if langauge + region tag can fallback to the one without
// region (e.g. fr-CA → fr)
lang = inOpts.language.split('-')[0];
if (!locales[lang]) {
lang = false;
}
}
}
delete inOpts.language;
if (lang) {
language = config.language = lang;
// update locale as well when updating language
const origLocale = locale || locales[defaultLang];
// use default language's properties for the fallback
locale = Object.assign({
format: defaultFormat,
weekStart: defaultWeekStart
}, locales[defaultLang]);
if (language !== defaultLang) {
Object.assign(locale, locales[language]);
}
config.locale = locale;
// if format and/or weekStart are the same as old locale's defaults,
// update them to new locale's defaults
if (format === origLocale.format) {
format = config.format = locale.format;
}
if (weekStart === origLocale.weekStart) {
weekStart = updateWeekStart(locale.weekStart, config, weekNumbers);
}
}
}
if (inOpts.format) {
const hasToDisplay = typeof inOpts.format.toDisplay === 'function';
const hasToValue = typeof inOpts.format.toValue === 'function';
const validFormatString = reFormatTokens.test(inOpts.format);
if ((hasToDisplay && hasToValue) || validFormatString) {
format = config.format = inOpts.format;
}
delete inOpts.format;
}
//*** pick level ***//
let newPickLevel = pickLevel;
if ('pickLevel' in inOpts) {
newPickLevel = validateViewId(inOpts.pickLevel, pickLevel, 2);
delete inOpts.pickLevel;
}
if (newPickLevel !== pickLevel) {
if (newPickLevel > pickLevel) {
// complement current minDate/madDate so that the existing range will be
// expanded to fit the new level later
if (!('minDate' in inOpts)) {
inOpts.minDate = minDate;
}
if (!('maxDate' in inOpts)) {
inOpts.maxDate = maxDate;
}
}
// complement datesDisabled so that it will be reset later
if (datesDisabled && !inOpts.datesDisabled) {
inOpts.datesDisabled = [];
}
pickLevel = config.pickLevel = newPickLevel;
}
//*** dates ***//
// while min and maxDate for "no limit" in the options are better to be null
// (especially when updating), the ones in the config have to be undefined
// because null is treated as 0 (= unix epoch) when comparing with time value
let minDt = minDate;
let maxDt = maxDate;
if ('minDate' in inOpts) {
const defaultMinDt = dateValue(0, 0, 1);
minDt = inOpts.minDate === null
? defaultMinDt // set 0000-01-01 to prevent negative values for year
: validateDate(inOpts.minDate, format, locale, minDt);
if (minDt !== defaultMinDt) {
minDt = regularizeDate(minDt, pickLevel, false);
}
delete inOpts.minDate;
}
if ('maxDate' in inOpts) {
maxDt = inOpts.maxDate === null
? undefined
: validateDate(inOpts.maxDate, format, locale, maxDt);
if (maxDt !== undefined) {
maxDt = regularizeDate(maxDt, pickLevel, true);
}
delete inOpts.maxDate;
}
if (maxDt < minDt) {
minDate = config.minDate = maxDt;
maxDate = config.maxDate = minDt;
} else {
if (minDate !== minDt) {
minDate = config.minDate = minDt;
}
if (maxDate !== maxDt) {
maxDate = config.maxDate = maxDt;
}
}
if (inOpts.datesDisabled) {
const dtsDisabled = inOpts.datesDisabled;
if (typeof dtsDisabled === 'function') {
config.datesDisabled = null;
config.checkDisabled = (timeValue, viewId) => dtsDisabled(
new Date(timeValue),
viewId,
rangeEnd
);
} else {
const disabled = config.datesDisabled = dtsDisabled.reduce((dates, dt) => {
const date = parseDate(dt, format, locale);
return date !== undefined
? pushUnique(dates, regularizeDate(date, pickLevel, rangeEnd))
: dates;
}, []);
config.checkDisabled = timeValue => disabled.includes(timeValue);
}
delete inOpts.datesDisabled;
}
if ('defaultViewDate' in inOpts) {
const viewDate = parseDate(inOpts.defaultViewDate, format, locale);
if (viewDate !== undefined) {
config.defaultViewDate = viewDate;
}
delete inOpts.defaultViewDate;
}
//*** days of week ***//
if ('weekStart' in inOpts) {
const wkStart = Number(inOpts.weekStart) % 7;
if (!isNaN(wkStart)) {
weekStart = updateWeekStart(wkStart, config, weekNumbers);
}
delete inOpts.weekStart;
}
if (inOpts.daysOfWeekDisabled) {
config.daysOfWeekDisabled = inOpts.daysOfWeekDisabled.reduce(sanitizeDOW, []);
delete inOpts.daysOfWeekDisabled;
}
if (inOpts.daysOfWeekHighlighted) {
config.daysOfWeekHighlighted = inOpts.daysOfWeekHighlighted.reduce(sanitizeDOW, []);
delete inOpts.daysOfWeekHighlighted;
}
//*** week numbers ***//
if ('weekNumbers' in inOpts) {
let method = inOpts.weekNumbers;
if (method) {
const getWeekNumber = typeof method === 'function'
? (timeValue, startOfWeek) => method(new Date(timeValue), startOfWeek)
: determineGetWeekMethod((method = parseInt(method, 10)), weekStart);
if (getWeekNumber) {
weekNumbers = config.weekNumbers = method;
config.getWeekNumber = getWeekNumber;
}
} else {
weekNumbers = config.weekNumbers = 0;
config.getWeekNumber = null;
}
delete inOpts.weekNumbers;
}
//*** multi date ***//
if ('maxNumberOfDates' in inOpts) {
const maxNumberOfDates = parseInt(inOpts.maxNumberOfDates, 10);
if (maxNumberOfDates >= 0) {
config.maxNumberOfDates = maxNumberOfDates;
config.multidate = maxNumberOfDates !== 1;
}
delete inOpts.maxNumberOfDates;
}
if (inOpts.dateDelimiter) {
config.dateDelimiter = String(inOpts.dateDelimiter);
delete inOpts.dateDelimiter;
}
//*** view ***//
let newMaxView = maxView;
if ('maxView' in inOpts) {
newMaxView = validateViewId(inOpts.maxView, maxView);
delete inOpts.maxView;
}
// ensure max view >= pick level
newMaxView = pickLevel > newMaxView ? pickLevel : newMaxView;
if (newMaxView !== maxView) {
maxView = config.maxView = newMaxView;
}
let newStartView = startView;
if ('startView' in inOpts) {
newStartView = validateViewId(inOpts.startView, newStartView);
delete inOpts.startView;
}
// ensure pick level <= start view <= max view
if (newStartView < pickLevel) {
newStartView = pickLevel;
} else if (newStartView > maxView) {
newStartView = maxView;
}
if (newStartView !== startView) {
config.startView = newStartView;
}
//*** template ***//
if (inOpts.prevArrow) {
const prevArrow = parseHTML(inOpts.prevArrow);
if (prevArrow.childNodes.length > 0) {
config.prevArrow = prevArrow.childNodes;
}
delete inOpts.prevArrow;
}
if (inOpts.nextArrow) {
const nextArrow = parseHTML(inOpts.nextArrow);
if (nextArrow.childNodes.length > 0) {
config.nextArrow = nextArrow.childNodes;
}
delete inOpts.nextArrow;
}
//*** misc ***//
if ('disableTouchKeyboard' in inOpts) {
config.disableTouchKeyboard = 'ontouchstart' in document && !!inOpts.disableTouchKeyboard;
delete inOpts.disableTouchKeyboard;
}
if (inOpts.orientation) {
const orientation = inOpts.orientation.toLowerCase().split(/\s+/g);
config.orientation = {
x: orientation.find(x => (x === 'left' || x === 'right')) || 'auto',
y: orientation.find(y => (y === 'top' || y === 'bottom')) || 'auto',
};
delete inOpts.orientation;
}
if ('todayButtonMode' in inOpts) {
switch(inOpts.todayButtonMode) {
case 0:
case 1:
config.todayButtonMode = inOpts.todayButtonMode;
}
delete inOpts.todayButtonMode;
}
//*** copy the rest ***//
Object.entries(inOpts).forEach(([key, value]) => {
if (value !== undefined && key in defaultOptions) {
config[key] = value;
}
});
return config;
}