birthdaypicker
Version:
i18n birthday picker with a variety of setting options
660 lines (659 loc) • 23.6 kB
JavaScript
/*!
* BirthdayPicker v0.2.2
* https://lemon3.github.io/birthdaypicker
*/
var H = Object.defineProperty;
var b = Object.getOwnPropertySymbols;
var W = Object.prototype.hasOwnProperty, $ = Object.prototype.propertyIsEnumerable;
var p = (a, i, t) => i in a ? H(a, i, { enumerable: !0, configurable: !0, writable: !0, value: t }) : a[i] = t, f = (a, i) => {
for (var t in i || (i = {}))
W.call(i, t) && p(a, t, i[t]);
if (b)
for (var t of b(i))
$.call(i, t) && p(a, t, i[t]);
return a;
};
var l = (a, i, t) => p(a, typeof i != "symbol" ? i + "" : i, t);
const Z = {
minAge: 0,
maxAge: 100,
minYear: null,
maxYear: "now",
lowerLimit: null,
upperLimit: null,
monthFormat: "short",
placeholder: !0,
className: null,
defaultDate: null,
autoInit: !0,
leadingZero: !0,
locale: "en",
selectFuture: !1,
arrange: "ymd",
yearEl: null,
monthEl: null,
dayEl: null,
roundDownDay: !0
}, C = {
en: { year: "Year", month: "Month", day: "Day" },
de: { year: "Jahr", month: "Monat", day: "Tag" },
fr: { year: "Année", month: "Mois", day: "Jour" }
}, q = (a, i, t, e) => {
if (i)
for (const s in i)
a.setAttribute(s, i[s]);
if (t)
for (const s in t)
a.style[s] = t[s];
return e && (a.innerHTML = e), a;
}, m = (a, i, t, e) => q(document.createElement(a), i, t, e), j = (a, i, t = null) => {
if (!a)
return !1;
if (a.dataset[i] === void 0)
return a.dataset;
let e;
try {
a.dataset[i] !== "undefined" && a.dataset[i].indexOf("{") >= 0 && (e = JSON.parse(a.dataset[i].replace(/'/g, '"')));
} catch (r) {
console.error(r);
}
if (typeof e != "object") {
e = a.dataset[i];
const r = {};
e = e.replace(/ /g, "");
const h = e.split(",");
h.length > 1 ? h.forEach((c) => {
const [u, k] = c.split(":");
r[u.replace(/'/g, "")] = k.replace(/'/g, "");
}) : r[i] = e, e = r;
}
let s = {}, n = i.length;
return Object.entries(a.dataset).forEach((r) => {
if (r[0].toLowerCase().indexOf(i) >= 0 && r[0].length > n) {
let h = r[0][n].toLowerCase() + r[0].substring(n + 1);
(t === null || t && t[h] !== void 0) && (s[h] = r[1]);
}
}), Object.assign(e, s);
}, w = (a) => +a % 4 === 0 && +a % 100 !== 0 || +a % 400 === 0, D = {
// storage
_s: /* @__PURE__ */ new WeakMap(),
put(a, ...i) {
this._s.has(a) || this._s.set(a, /* @__PURE__ */ new Map());
let t = this._s.get(a);
if (i.length > 1)
return t.set(i[0], i[1]), this;
if (typeof i[0] == "object")
for (const e in i[0])
t.set(e, i[0][e]);
else
t.set(i[0]);
return this;
},
get(a, i) {
return this._s.has(a) ? i ? this._s.get(a).get(i) : this._s.get(a) : !1;
},
has(a, i) {
return this._s.has(a) && this._s.get(a).has(i);
},
// todo if no key given: remove all
remove(a, i) {
if (!this._s.has(a))
return !1;
let t = this._s.get(a).delete(i);
return this._s.get(a).size === 0 && this._s.delete(a), t;
}
}, L = (a, i, t) => {
if (a = parseFloat(a, 10), isNaN(a))
return NaN;
if (i = parseFloat(i, 10), t = parseFloat(t, 10), t < i) {
let e = t;
t = i, i = e;
}
return !isNaN(i) && a < i ? i : !isNaN(t) && a > t ? t : a;
}, _ = (a) => a === !0 || a === "true" || a === 1 || a === "1", z = (a, i, t) => {
let e = a.getAttribute("on" + i);
new Function(
"e",
// 'with(document) {' +
// 'with(this)' +
"{" + e + "}"
// + '}'
).call(a, t);
};
class G {
constructor() {
this._eventCallbacks = this._eventCallbacks || {};
}
emit(i, t) {
let e = this._eventCallbacks[i];
const s = { bubbles: !1, cancelable: !1, detail: t }, n = new CustomEvent(i, s);
e && e.forEach((r) => r.call(this, n)), this.element && (this.element.dispatchEvent(n), z(this.element, i, n));
}
// on
addEventListener(i, t) {
return this.allowedEvents && this.allowedEvents.indexOf(i) < 0 || typeof t != "function" ? !1 : (this._eventCallbacks[i] || (this._eventCallbacks[i] = []), this._eventCallbacks[i].push(t), this);
}
// off
removeEventListener(i, t) {
if (!this._eventCallbacks || arguments.length === 0)
return this._eventCallbacks = {}, this;
let e = this._eventCallbacks[i];
return e ? arguments.length === 1 ? (delete this._eventCallbacks[i], this) : (this._eventCallbacks[i] = e.filter(
(s) => s !== t
), this) : this;
}
}
let d = [];
const O = "birthdaypicker", Y = "data-" + O, J = ["short", "long", "numeric"], R = ["ymd", "ydm", "myd", "mdy", "dmy", "dym"], v = "init", E = "datechange", S = "daychange", T = "monthchange", x = "yearchange", A = "kill", M = "option", y = { y: "year", m: "month", d: "day" }, N = /* @__PURE__ */ new Date(), g = {
y: N.getFullYear(),
m: N.getMonth() + 1,
d: N.getDate()
};
let F = !1;
const o = class o extends G {
constructor(t, e) {
if (!t)
return { error: !0 };
if (t = typeof t == "string" ? document.querySelector(t) : t, t === null || t.length === 0)
return { error: !0 };
super();
/**
* date change event handler, called if one of the fields is updated
* @param {Event} e The event
* @return {void}
*/
l(this, "_onSelect", (t) => {
t.target === this._year.el ? this._yearWasChanged(+t.target.value) : t.target === this._month.el ? this._monthWasChanged(+t.target.value) : t.target === this._day.el && this._dayWasChanged(+t.target.value), this._dateChanged();
});
if (t.dataset.bdpInit)
return o.getInstance(t);
t.dataset.bdpInit = !0, this.allowedEvents = [
v,
E,
S,
T,
x,
A
], d.push(this), D.put(t, "instance", this);
const s = j(t, O, o.defaults);
this.options = e || {}, this.settings = Object.assign({}, o.defaults, s, e), this.element = t, this.state = 0, this.settings.autoInit && this.init();
}
_triggerEvent(t, e) {
e = f({
instance: this,
year: +this.currentYear,
month: +this.currentMonth,
day: +this.currentDay,
date: this.getDate()
}, e), this.emit(t, e);
}
/**
* Parses a given date string or Date object
*
* @param {*} date A data string or a Date object (new Date())
* @returns object with { year, month, day } or false if it is not a correct date
*/
_parseDate(t) {
if (t instanceof Date && !isNaN(t))
return {
year: t.getFullYear(),
month: t.getMonth() + 1,
day: t.getDate()
};
if (typeof t != "string" || !t.trim())
return !1;
const e = new Date(t.replace(/-/g, "/"));
return isNaN(e) ? !1 : {
year: e.getFullYear(),
month: e.getMonth() + 1,
day: e.getDate()
};
}
/**
* Function to return the index of a chosen value for a given NodeList
* @param {NodeList} nodeList Option List
* @param {String|Number} value Value to find
* @return {Number|undefined} The index value or undefined
*/
_getIdx(t, e) {
if (!(t instanceof NodeList) || e === void 0 || e === null)
return;
const s = isNaN(e) ? e : +e, n = Array.from(t).findIndex(
(r) => +r.value === s
);
return n !== -1 ? n : void 0;
}
/**
* Updates one selectBox
* @param {String} box name of the box ('_year', '_month', '_day')
* @param {number} value the new value to which the box should be set
*/
_updateSelectBox(t, e) {
const s = this[t].el;
s.selectedIndex = this._getIdx(s.childNodes, e);
}
/**
* set the year to a given value
* and change the corresponding select-box too.
* @param {String|Int} year the day value (eg, 1988, 2012, ...)
* @returns
*/
_setYear(t) {
return t = L(t, this._yearFrom, this._yearTo), this.currentYear === t ? !1 : (this._updateSelectBox("_year", t), this._yearWasChanged(t, !1), !0);
}
/**
* set the month to a given value
* and change the corresponding select-box too.
* @param {String|Int} month the month value (usually between 1 - 12)
* @returns
*/
_setMonth(t) {
return t = L(t, 1, 12), this.currentMonth === t ? !1 : (this._updateSelectBox("_month", t), this._monthWasChanged(t, !1), !0);
}
/**
* set the day to a given value
* and change the corresponding select-box too.
* @param {String|Int} day the day value (usually between 1 - 31)
* @returns
*/
_setDay(t) {
return t = L(t, 1, this._getDaysPerMonth()), this.currentDay === t ? !1 : (this._updateSelectBox("_day", t), this._dayWasChanged(t, !1), !0);
}
_getDateInRange({
year: t = this.currentYear,
month: e = this.currentMonth,
day: s = this.currentDay
} = {}) {
const n = this._lowerLimit, r = this._upperLimit;
if (!n || !r)
throw new Error("Lower and upper date limits must be defined.");
const h = new Date(t, e - 1, s), c = new Date(n.year, n.month - 1, n.day), u = new Date(r.year, r.month - 1, r.day);
return h > u ? r : h < c ? n : { year: t, month: e, day: s };
}
/**
* Set the date
* @param {Object} obj with year, month, day as String or Integer
* @param {Number} obj.year the year value
* @param {Number} obj.month the month value
* @param {Number} obj.day the day value
*/
_setDate(t, e = !1) {
t = this._getDateInRange(t);
let s = this._setYear(t.year), n = this._setMonth(t.month), r = this._setDay(t.day);
return (s || n || r) && this._dateChanged(e), t;
}
// function for update or create
_getMonthText(t) {
return this.settings.monthFormat !== "numeric" ? t : this.settings.leadingZero ? String(t).padStart(2, "0") : String(t);
}
_checkArrangement(t) {
return t = t.toLowerCase(), R.includes(t) ? t : "ymd";
}
_mapSelectBoxes() {
const t = this.element.querySelectorAll("select");
if (!t.length) return {};
const e = {}, s = this.settings.arrange.split(""), n = s.map((h) => y[h]);
n.forEach((h) => {
const c = this.settings[`${h}El`] || `[${Y}-${h}]`, u = c.nodeName ? c : this.element.querySelector(c);
u && u.tagName === "SELECT" && (e[h] = u, s.splice(
s.indexOf(n.indexOf(h).toString()),
1
));
});
const r = Array.from(t).filter(
(h) => !Object.values(e).includes(h)
);
return s.forEach((h, c) => {
const u = y[h];
e[u] = r[c];
}), e;
}
/**
* Create the GUI
* @return {void}
*/
_create() {
const t = this.settings;
t.arrange = this._checkArrangement(t.arrange);
const e = this._mapSelectBoxes();
t.arrange.split("").forEach((n) => {
const r = y[n];
let h = e[r];
(!h || h.dataset.init) && (h = m("select"), this.element.append(h)), h.setAttribute("aria-label", `select ${r}`), t.className && h.classList.add(t.className, `${t.className}-${r}`), h.dataset.init = !0;
const c = this._onSelect;
h.addEventListener("change", c, !1), this._registeredEventListeners.push({
element: h,
eventName: "change",
listener: c,
option: !1
}), this[`_${r}`] = {
el: h,
name: r,
created: !e[r],
df: document.createDocumentFragment()
}, this._date.push(this[`_${r}`]);
});
const s = m(M);
t.placeholder && this._date.forEach((n) => {
const r = o.i18n[t.locale][n.name], h = s.cloneNode();
h.innerHTML = r, n.df.appendChild(h);
});
for (let n = this._yearTo; n >= this._yearFrom; n--) {
const r = s.cloneNode();
r.value = n, r.innerHTML = n, this._year.df.append(r);
}
this.monthFormat[t.monthFormat].forEach((n, r) => {
const h = s.cloneNode();
h.value = r + 1, h.innerHTML = this._getMonthText(n), this._month.df.append(h);
});
for (let n = 1; n <= 31; n++) {
const r = t.leadingZero && n < 10 ? `0${n}` : n, h = m(M, { value: n }, "", r);
this._day.df.append(h);
}
this._date.forEach((n) => n.el.append(n.df));
}
/**
* Update the days according to the given month
* called when month changes or year changes form non-leap-year to a leap-year
* or vice versa
* @param {number} [month=this.currentMonth] The number of the month (1-12)
* @return {void}
*/
_updateDays(t = this.currentMonth) {
const e = this._getDaysPerMonth(t) || 31, s = this.settings.placeholder ? 1 : 0, n = this._day.el.children.length - s;
if (e === n) return;
const r = e - n;
if (r > 0)
for (let h = n + 1; h <= e; h++) {
const c = m(M, { value: h }, "", h);
this._day.el.append(c);
}
else {
for (let h = 0; h < -r; h++)
this._day.el.children[n + s - h - 1].remove();
this.currentDay > e && (this.settings.roundDownDay ? this._setDay(e) : this._dayWasChanged());
}
}
/**
* Disable the options in the select box
*
* @param {String} selectBox - Name of the select box (_year, _month, _day)
* @param {String} condition - '>' or '<'
* @param {Number} limit - the limit
*/
_disable(t, e, s) {
const n = e === "<";
this[t].el.childNodes.forEach((r) => {
(n && +r.value < s || !n && +r.value > s) && (r.disabled = !0, this._disabled.push(r));
});
}
// TODO: only on select change
// TODO: only on select change
_noFutureDate(t = this._lowerLimit, e = this._upperLimit) {
const s = () => {
this.currentYear !== e.year && this._setYear(e.year), this._disable("_month", ">", e.month);
let r = this.currentMonth > e.month;
r && this._setMonth(e.month), e.month === this.currentMonth && (this._disable("_day", ">", e.day), (this.currentDay > e.day || r && this.currentDay < e.day) && this._setDay(e.day));
}, n = () => {
this.currentYear !== t.year && this._setYear(t.year), this._disable("_month", "<", t.month);
let r = this.currentMonth < t.month;
r && this._setMonth(t.month), t.month === this.currentMonth && (this._disable("_day", "<", t.day), (this.currentDay < t.day || r && this.currentDay > t.day) && this._setDay(t.day));
};
return this._disabled.forEach((r) => {
r.disabled = !1;
}), this._disabled = [], this.currentYear < e.year && this.currentYear > t.year || !this.currentYear || !this.currentYear && !this.currentMonth && !this.currentDay ? !1 : (this.currentYear >= e.year ? s() : this.currentYear <= t.year && n(), !0);
}
/**
* called if the year was changed
* sets the currentYear value and triggers the corresponding event
* @param {number} year the new year value
* @returns
*/
_yearWasChanged(t, e = !0) {
this.currentYear = t, this._daysPerMonth[1] = w(t) ? 29 : 28, e && this._triggerEvent(x), this.currentMonth === 2 && this._updateDays();
}
/**
* called if the month was changed
* sets the currentMonth value and triggers the corresponding event
* @param {number} month the new month value
* @returns
*/
_monthWasChanged(t, e = !0) {
this.currentMonth = t, e && this._triggerEvent(T), this._updateDays();
}
/**
* called if the day was changed
* sets the currentDay value and triggers the corresponding event
* @param {number} day the new day value
* @returns
*/
_dayWasChanged(t, e = !0) {
this.currentDay = t, e && this._triggerEvent(S);
}
/**
* called if one value was changed
*
* @param {boolean} [triggerEvent = true] - true if the event should be triggered, default: true
*/
_dateChanged(t = !0) {
this.settings.selectFuture || this._noFutureDate(this._lowerLimit, this._upperLimit), t && this._triggerEvent(E), this.element.value = this.getDateString();
}
/**
* updates the innerHTML off the day option list
* adds or removes a leading zero depending on the value of
* this.settings.leadingZero
*/
_updateDayList() {
const t = this.settings.placeholder ? 1 : 0, e = this.settings.leadingZero ? "0" : "";
for (let s = t; s < 9 + t; s++)
this._day.el.childNodes[s].innerHTML = e + s;
}
/**
* updates the innerHTML off the month option list
* adds or removes a leading zero depending on the value of
* this.settings.leadingZero or changes the month format
*/
_updateMonthList() {
const t = this.settings.placeholder ? 1 : 0, e = this.settings.monthFormat;
this.monthFormat[e].forEach((s, n) => {
this._month.el.childNodes[n + t].innerHTML = this._getMonthText(s);
});
}
/**
* Set the GUI (select boxes) to use a leading zero or not
*
* @param {boolean} leadingZero
*/
useLeadingZero(t) {
t = _(t), t !== this.settings.leadingZero && (this.settings.leadingZero = t, this.settings.monthFormat === "numeric" && this._updateMonthList(), this._updateDayList());
}
/**
* Returns the number of days available for the given month
*
* @param {Number} month the month usually between 1-12 ;)
* @returns number of days
*/
_getDaysPerMonth(t = this.currentMonth) {
return this._daysPerMonth[+t - 1];
}
/**
* Change the current active month format
*
* @param {String} format the format string, available: 'short', 'long', 'numeric';
* @returns {boolean} true if changed, false if not
*/
setMonthFormat(t) {
return !this.monthFormat[t] || t === this.settings.monthFormat ? !1 : (this.settings.monthFormat = t, this._updateMonthList(), !0);
}
setLanguage(t) {
if (t === this.settings.locale || ("" + t).length < 2 || ("" + t).length > 2)
return !1;
o.createLocale(t);
const e = o.i18n[t];
if (this.settings.placeholder && this._date.forEach((n) => {
n.el.childNodes[0].innerHTML = e[n.name];
}), this.monthFormat = e.monthFormat, this.settings.locale = t, this.settings.monthFormat === "numeric")
return !1;
let s = this.settings.placeholder ? 1 : 0;
this.monthFormat[this.settings.monthFormat].forEach((n, r) => {
this._month.el.childNodes[s + r].innerHTML = n;
});
}
setDate(t, e = !1) {
let s = this._parseDate(t);
return s && (s = this._setDate(s, e)), s;
}
resetDate(t = !1) {
const e = t && this.startDate ? this.startDate : { year: NaN, month: NaN, day: NaN };
this._setDate(e), this.element.value = this.getDateString(), this._triggerEvent(E);
}
// TODO: undo everything
// destroy (cleanup) if it was created
kill() {
var t;
(t = this._registeredEventListeners) == null || t.forEach(
(e) => e.element.removeEventListener(e.eventName, e.listener, e.option)
), this._date.forEach((e) => {
if (e.created)
e.el.remove();
else {
const s = this.settings.className;
s && e.el.classList.remove(
s,
...Object.values(y).map((n) => `${s}-${n}`)
);
}
}), this._triggerEvent(A);
}
isLeapYear(t = this.currentYear) {
return t === void 0 ? void 0 : w(t);
}
getAge() {
if (isNaN(this.currentYear) || isNaN(this.currentMonth) || isNaN(this.currentDay))
return "";
const t = this.currentYear, e = this.currentMonth, s = this.currentDay, n = /* @__PURE__ */ new Date(), r = n.getMonth() + 1;
return n.getFullYear() - t - (r < e || r === e && n.getDate() < s ? 1 : 0);
}
getDateString(t) {
if (!t) {
const r = this.getDate();
return r && r.toLocaleDateString(this.settings.locale);
}
if (!this.currentYear || !this.currentMonth || !this.currentDay) return "";
const e = this.currentYear, s = String(this.currentMonth).padStart(2, "0"), n = String(this.currentDay).padStart(2, "0");
return t.toLowerCase().replace(/yyyy/g, e).replace(/yy/g, e.toString().slice(2)).replace(/mm/g, s).replace(/m/g, this.currentMonth).replace(/dd/g, n).replace(/d/g, this.currentDay);
}
getDate() {
return !this.currentYear || !this.currentMonth || !this.currentDay ? "" : new Date(
Date.UTC(this.currentYear, +this.currentMonth - 1, this.currentDay)
);
}
/**
*
* @param {Object} s - the settings object
* @returns Object containing { year, month, day }
*/
_getUpperLimit() {
const t = this.settings;
if (t.upperLimit)
return t.upperLimit;
let e;
return t.maxYear === "now" ? (e = g.y - +t.minAge, { year: e, month: g.m, day: g.d }) : (e = t.maxYear, { year: e, month: 12, day: 31 });
}
/**
*
* @param {Object} s - the settings object
* @returns Object containing { year, month, day }
*/
_getLowerLimit() {
const t = this.settings;
if (t.lowerLimit)
return t.lowerLimit;
let e;
return t.minYear !== null ? (e = +t.minYear, { year: e, month: 1, day: 1 }) : (e = (t.maxYear === "now" ? g.y : t.maxYear) - +t.maxAge, { year: e, month: g.m, day: g.d });
}
/**
* The init method
* TODO: test all(!) option values for correctness
*
* @return {*}
* @memberof BirthdayPicker
*/
init() {
if (this.initialized)
return !0;
this.initialized = !0, this._registeredEventListeners = [], this._daysPerMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], this._date = [], this._disabled = [];
const t = this.settings;
t.placeholder = _(t.placeholder), t.leadingZero = _(t.leadingZero), t.selectFuture = _(t.selectFuture), this._lowerLimit = this._getLowerLimit(), this._upperLimit = this._getUpperLimit(), this._yearFrom = this._lowerLimit.year, this._yearTo = this._upperLimit.year;
const [e] = o.createLocale(t.locale);
if (t.locale = e, this.monthFormat = o.i18n[t.locale].monthFormat, this.allowedEvents.forEach((s) => {
t[s] && this.addEventListener(s, t[s]);
}), this._create(), t.defaultDate) {
const s = this.setDate(
t.defaultDate === "now" ? (/* @__PURE__ */ new Date()).toString() : t.defaultDate,
!0
);
this.startDate = s;
}
this.state = 1, this._triggerEvent(v);
}
};
l(o, "currentLocale", "en"), l(o, "i18n", {}), l(o, "createLocale", (t = "en") => {
if (t.length !== 2 && (t = "en"), o.i18n[t])
return [t, o.i18n[t]];
const e = /* @__PURE__ */ new Date("2000-01-15"), s = { monthFormat: {} };
J.forEach((h) => {
s.monthFormat[h] = [];
for (let c = 0; c < 12; c++)
e.setMonth(c), s.monthFormat[h].push(
e.toLocaleDateString(t, { month: h })
);
});
const n = "BirthdayPickerLocale", r = f(f({}, C[t] || C.en), window[n] && window[n][t]);
return o.i18n[t] = f(f({}, s), r), [t, o.i18n[t]];
}), l(o, "getInstance", (t) => D.get(t, "instance")), /**
* Set the month format for all registered instances
* @param {String} format The available formats are: 'short', 'long', 'numeric'
*/
l(o, "setMonthFormat", (t) => {
d.forEach((e) => {
e.setMonthFormat(t);
});
}), /**
* Set the language of all registered instances
* @param {String} lang The language string, eg.: 'en', 'de'
*/
l(o, "setLanguage", (t) => {
o.currentLocale = t, d.forEach((e) => {
e.setLanguage(t);
});
}), /**
* Kill all events on all registered instances
* @returns {Boolean} false if no instance was found, or true if events where removed
*/
l(o, "killAll", () => d.length ? (d.forEach((t) => {
o.kill(t);
}), d = [], !0) : !1), /**
*
* @param {*} instance either an registered html object or an instance of the BirthdayPicker class
* @returns {Boolean} false or true
*/
l(o, "kill", (t) => {
if (!t || (t.element || (t = o.getInstance(t)), !t))
return !1;
t.kill();
const e = t.element;
return e.dataset.bdpInit = !1, delete e.dataset.bdpInit, D.remove(e, "instance"), F = !1, !0;
}), l(o, "defaults", Z), l(o, "init", () => {
if (F)
return !1;
F = !0, o.createLocale(o.currentLocale);
let t = document.querySelectorAll("[" + Y + "]");
return t.length === 0 ? !1 : (t.forEach((e) => {
new o(e);
}), d);
});
let I = o;
export {
I as default
};