mirror of
https://github.com/tabler/tabler.git
synced 2026-10-22 15:47:27 +04:00
Add dateFormat patterns and boundInput to Datepicker (#3260)
This commit is contained in:
1 parent
00746f2446
commit
964eb7cfee
4 files changed
+247
-21
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": minor
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added `dateFormat` patterns such as `iso` or `DD.MM.YYYY` and a `boundInput` option for ISO form values to `Datepicker`.
|
||||||
+111
-17
@@ -39,6 +39,8 @@ const SELECTOR_BOUND_INPUT = 'input[type="hidden"], input[name]'
|
|||||||
const HIDE_DELAY = 100 // ms delay before hiding after selection
|
const HIDE_DELAY = 100 // ms delay before hiding after selection
|
||||||
|
|
||||||
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/
|
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/
|
||||||
|
const FORMAT_TOKENS = /YYYY|MM|M|DD|D/g
|
||||||
|
const ISO_FORMAT = 'YYYY-MM-DD'
|
||||||
|
|
||||||
// Vanilla Calendar Pro 3.4 moved several months, the time picker, week
|
// Vanilla Calendar Pro 3.4 moved several months, the time picker, week
|
||||||
// numbers, popups and animation into extensions that must be registered when
|
// numbers, popups and animation into extensions that must be registered when
|
||||||
@@ -85,8 +87,13 @@ type ComponentConfig = {
|
|||||||
datepickerTheme: string | null
|
datepickerTheme: string | null
|
||||||
dateMin: DateAny | null
|
dateMin: DateAny | null
|
||||||
dateMax: DateAny | null
|
dateMax: DateAny | null
|
||||||
/** `Intl.DateTimeFormat` options, or a function(date, locale) returning the text shown in the field */
|
/** input that receives the picked dates as `YYYY-MM-DD`, comma-separated; an inline calendar finds a hidden one inside */
|
||||||
dateFormat: Intl.DateTimeFormatOptions | ((date: Date, locale: string | undefined) => string) | null
|
boundInput: string | HTMLInputElement | null
|
||||||
|
/**
|
||||||
|
* How the date is written into the field: a pattern such as `'DD.MM.YYYY'` (tokens `YYYY`, `MM`, `M`, `DD`, `D`),
|
||||||
|
* `'iso'`, `Intl.DateTimeFormat` options, or a function(date, locale) returning the text
|
||||||
|
*/
|
||||||
|
dateFormat: string | Intl.DateTimeFormatOptions | ((date: Date, locale: string | undefined) => string) | null
|
||||||
/** element that shows the formatted date; a button uses itself or its `[data-bs-datepicker-display]` child */
|
/** element that shows the formatted date; a button uses itself or its `[data-bs-datepicker-display]` child */
|
||||||
displayElement: string | HTMLElement | boolean | null
|
displayElement: string | HTMLElement | boolean | null
|
||||||
displayMonthsCount: MonthsCount
|
displayMonthsCount: MonthsCount
|
||||||
@@ -112,6 +119,7 @@ type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
|||||||
type ChangeEventArgs = { dates: string[]; event: MouseEvent }
|
type ChangeEventArgs = { dates: string[]; event: MouseEvent }
|
||||||
|
|
||||||
const Default: ComponentConfig = {
|
const Default: ComponentConfig = {
|
||||||
|
boundInput: null,
|
||||||
datepickerTheme: null,
|
datepickerTheme: null,
|
||||||
dateMin: null,
|
dateMin: null,
|
||||||
dateMax: null,
|
dateMax: null,
|
||||||
@@ -130,10 +138,11 @@ const Default: ComponentConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||||
|
boundInput: '(null|string|element)',
|
||||||
datepickerTheme: '(null|string)',
|
datepickerTheme: '(null|string)',
|
||||||
dateMin: '(null|string|number|object|date)',
|
dateMin: '(null|string|number|object|date)',
|
||||||
dateMax: '(null|string|number|object|date)',
|
dateMax: '(null|string|number|object|date)',
|
||||||
dateFormat: '(null|object|function)',
|
dateFormat: '(null|string|object|function)',
|
||||||
displayElement: '(null|string|element|boolean)',
|
displayElement: '(null|string|element|boolean)',
|
||||||
displayMonthsCount: 'number',
|
displayMonthsCount: 'number',
|
||||||
firstWeekday: 'number',
|
firstWeekday: 'number',
|
||||||
@@ -147,6 +156,43 @@ const DefaultType: Record<keyof ComponentConfig, string> = {
|
|||||||
vcpOptions: 'object',
|
vcpOptions: 'object',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pad = (value: number, length = 2): string => String(value).padStart(length, '0')
|
||||||
|
|
||||||
|
const formatPattern = (date: Date, pattern: string): string => {
|
||||||
|
const parts: Record<string, string> = {
|
||||||
|
YYYY: pad(date.getFullYear(), 4),
|
||||||
|
MM: pad(date.getMonth() + 1),
|
||||||
|
M: String(date.getMonth() + 1),
|
||||||
|
DD: pad(date.getDate()),
|
||||||
|
D: String(date.getDate()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return pattern.replace(FORMAT_TOKENS, (token) => parts[token])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads text written with `pattern` back into `YYYY-MM-DD`, or null when it
|
||||||
|
// does not match or names a day that does not exist.
|
||||||
|
const parsePattern = (value: string, pattern: string): string | null => {
|
||||||
|
const order: string[] = []
|
||||||
|
const source = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(FORMAT_TOKENS, (token) => {
|
||||||
|
order.push(token[0])
|
||||||
|
return token === 'YYYY' ? '(\\d{4})' : token.length === 2 ? '(\\d{2})' : '(\\d{1,2})'
|
||||||
|
})
|
||||||
|
const match = new RegExp(`^${source}$`).exec(value)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: Record<string, number> = { Y: 0, M: 1, D: 1 }
|
||||||
|
order.forEach((key, index) => {
|
||||||
|
parts[key] = Number(match[index + 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
const { Y: year = 0, M: month = 1, D: day = 1 } = parts
|
||||||
|
const date = new Date(year, month - 1, day)
|
||||||
|
return date.getMonth() === month - 1 && date.getDate() === day ? `${pad(year, 4)}-${pad(month)}-${pad(day)}` : null
|
||||||
|
}
|
||||||
|
|
||||||
// Delegated Data API handlers run in the capture phase, before the plugin's
|
// Delegated Data API handlers run in the capture phase, before the plugin's
|
||||||
// own click listener, which opens the popup from a `setTimeout`. Two nested
|
// own click listener, which opens the popup from a `setTimeout`. Two nested
|
||||||
// timeouts are the first point that is after it for certain.
|
// timeouts are the first point that is after it for certain.
|
||||||
@@ -181,6 +227,7 @@ class Datepicker extends BaseComponent {
|
|||||||
_isInput = false
|
_isInput = false
|
||||||
_isInline = false
|
_isInline = false
|
||||||
_boundInput: HTMLInputElement | null = null
|
_boundInput: HTMLInputElement | null = null
|
||||||
|
_boundInputInside = false
|
||||||
_positionElement: HTMLElement | null = null
|
_positionElement: HTMLElement | null = null
|
||||||
_displayElement: HTMLElement | false | null = null
|
_displayElement: HTMLElement | false | null = null
|
||||||
_themeObserver: MutationObserver | null = null
|
_themeObserver: MutationObserver | null = null
|
||||||
@@ -314,7 +361,7 @@ class Datepicker extends BaseComponent {
|
|||||||
if (clone && clone !== this._element && clone.isConnected) {
|
if (clone && clone !== this._element && clone.isConnected) {
|
||||||
if (this._isInput) {
|
if (this._isInput) {
|
||||||
clone.replaceWith(this._element)
|
clone.replaceWith(this._element)
|
||||||
} else if (this._boundInput) {
|
} else if (this._boundInput && this._boundInputInside) {
|
||||||
const copy = SelectorEngine.findOne(SELECTOR_BOUND_INPUT, clone)
|
const copy = SelectorEngine.findOne(SELECTOR_BOUND_INPUT, clone)
|
||||||
if (copy) {
|
if (copy) {
|
||||||
copy.replaceWith(this._boundInput)
|
copy.replaceWith(this._boundInput)
|
||||||
@@ -334,8 +381,7 @@ class Datepicker extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelectedDates(dates: DatesArr): void {
|
setSelectedDates(dates: DatesArr): void {
|
||||||
this._selectedDates = dates.map((date) => this._toIsoDate(date))
|
this._select(dates.map((date) => this._toIsoDate(date)))
|
||||||
this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
|
|
||||||
this._writeSelection(this._selectedDates)
|
this._writeSelection(this._selectedDates)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,10 +390,8 @@ class Datepicker extends BaseComponent {
|
|||||||
this._isInput = this._element.tagName === 'INPUT'
|
this._isInput = this._element.tagName === 'INPUT'
|
||||||
this._isInline = this._config.inline
|
this._isInline = this._config.inline
|
||||||
|
|
||||||
// For inline mode, look for a hidden input child to bind to
|
this._boundInput = this._resolveBoundInput()
|
||||||
if (this._isInline && !this._isInput) {
|
this._boundInputInside = Boolean(this._boundInput && this._element.contains(this._boundInput))
|
||||||
this._boundInput = SelectorEngine.findOne(SELECTOR_BOUND_INPUT, this._element) as HTMLInputElement | null
|
|
||||||
}
|
|
||||||
|
|
||||||
this._positionElement = this._resolvePositionElement()
|
this._positionElement = this._resolvePositionElement()
|
||||||
this._displayElement = this._resolveDisplayElement()
|
this._displayElement = this._resolveDisplayElement()
|
||||||
@@ -376,6 +420,13 @@ class Datepicker extends BaseComponent {
|
|||||||
this._parseInputValue()
|
this._parseInputValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The field shows localized text the next page load cannot read back, so
|
||||||
|
// a value the server rendered into the bound input selects the dates.
|
||||||
|
const boundDates = this._boundInput?.value.split(',').filter((date) => DATE_PATTERN.test(date))
|
||||||
|
if (this._selectedDates.length === 0 && boundDates?.length) {
|
||||||
|
this._select(boundDates)
|
||||||
|
}
|
||||||
|
|
||||||
// Populate input/display with preselected dates
|
// Populate input/display with preselected dates
|
||||||
this._updateDisplayWithSelectedDates()
|
this._updateDisplayWithSelectedDates()
|
||||||
}
|
}
|
||||||
@@ -405,6 +456,17 @@ class Datepicker extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resolveBoundInput(): HTMLInputElement | null {
|
||||||
|
const { boundInput } = this._config
|
||||||
|
|
||||||
|
if (boundInput) {
|
||||||
|
return getElement(boundInput) as HTMLInputElement | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// An inline calendar submits through a hidden input inside it
|
||||||
|
return this._isInline && !this._isInput ? (SelectorEngine.findOne(SELECTOR_BOUND_INPUT, this._element) as HTMLInputElement | null) : null
|
||||||
|
}
|
||||||
|
|
||||||
_resolvePositionElement(): HTMLElement {
|
_resolvePositionElement(): HTMLElement {
|
||||||
let { positionElement } = this._config
|
let { positionElement } = this._config
|
||||||
|
|
||||||
@@ -689,6 +751,11 @@ class Datepicker extends BaseComponent {
|
|||||||
const locale = this._config.locale === 'default' ? undefined : this._config.locale
|
const locale = this._config.locale === 'default' ? undefined : this._config.locale
|
||||||
const { dateFormat } = this._config
|
const { dateFormat } = this._config
|
||||||
|
|
||||||
|
// Token pattern, independent of the locale
|
||||||
|
if (typeof dateFormat === 'string') {
|
||||||
|
return formatPattern(date, this._formatPattern()!)
|
||||||
|
}
|
||||||
|
|
||||||
// Custom function formatter
|
// Custom function formatter
|
||||||
if (typeof dateFormat === 'function') {
|
if (typeof dateFormat === 'function') {
|
||||||
return dateFormat(date, locale)
|
return dateFormat(date, locale)
|
||||||
@@ -703,6 +770,20 @@ class Datepicker extends BaseComponent {
|
|||||||
return date.toLocaleDateString(locale)
|
return date.toLocaleDateString(locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_formatPattern(): string | null {
|
||||||
|
const { dateFormat } = this._config
|
||||||
|
|
||||||
|
if (typeof dateFormat !== 'string') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateFormat.toLowerCase() === 'iso' ? ISO_FORMAT : dateFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
_dateSeparator(): string {
|
||||||
|
return this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '
|
||||||
|
}
|
||||||
|
|
||||||
_formatDateForInput(dates: string[]): string {
|
_formatDateForInput(dates: string[]): string {
|
||||||
if (dates.length === 0) {
|
if (dates.length === 0) {
|
||||||
return ''
|
return ''
|
||||||
@@ -713,8 +794,7 @@ class Datepicker extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For date ranges, use en-dash; for multiple dates, use comma
|
// For date ranges, use en-dash; for multiple dates, use comma
|
||||||
const separator = this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '
|
return dates.map((date) => this._formatDate(date)).join(this._dateSeparator())
|
||||||
return dates.map((date) => this._formatDate(date)).join(separator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseInputValue(): void {
|
_parseInputValue(): void {
|
||||||
@@ -725,11 +805,21 @@ class Datepicker extends BaseComponent {
|
|||||||
|
|
||||||
const written = writtenSelections.get(this._element)
|
const written = writtenSelections.get(this._element)
|
||||||
if (written && written.text.trim() === value) {
|
if (written && written.text.trim() === value) {
|
||||||
this._selectedDates = [...written.dates]
|
this._select(written.dates)
|
||||||
this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A pattern reads back what it wrote, e.g. a value the server sent again
|
||||||
|
const pattern = this._formatPattern()
|
||||||
|
if (pattern) {
|
||||||
|
const parts = this._config.selectionMode === 'single' ? [value] : value.split(this._dateSeparator())
|
||||||
|
const dates = parts.map((part) => parsePattern(part.trim(), pattern))
|
||||||
|
if (dates.every(Boolean)) {
|
||||||
|
this._select(dates as string[])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// `YYYY-MM-DD` is read as a local date: `new Date('2020-06-20')` would be
|
// `YYYY-MM-DD` is read as a local date: `new Date('2020-06-20')` would be
|
||||||
// UTC midnight and land on the previous day west of Greenwich.
|
// UTC midnight and land on the previous day west of Greenwich.
|
||||||
const date = DATE_PATTERN.test(value) ? this._parseDate(value) : new Date(value)
|
const date = DATE_PATTERN.test(value) ? this._parseDate(value) : new Date(value)
|
||||||
@@ -737,8 +827,12 @@ class Datepicker extends BaseComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this._selectedDates = [this._toIsoDate(date)]
|
this._select([this._toIsoDate(date)])
|
||||||
// The calendar opens on the month of the value, not on today.
|
}
|
||||||
|
|
||||||
|
_select(dates: string[]): void {
|
||||||
|
this._selectedDates = [...dates]
|
||||||
|
// The calendar opens on the month of the selection, not on today.
|
||||||
this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
|
this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,7 +892,7 @@ EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE, function
|
|||||||
initAll(SELECTOR_DATA_TOGGLE, Datepicker, (element) => {
|
initAll(SELECTOR_DATA_TOGGLE, Datepicker, (element) => {
|
||||||
const { dataset } = element
|
const { dataset } = element
|
||||||
|
|
||||||
return dataset.bsInline === 'true' || dataset.tblrInline === 'true' || 'bsSelectedDates' in dataset || 'tblrSelectedDates' in dataset || Boolean((element as HTMLInputElement).value)
|
return dataset.bsInline === 'true' || dataset.tblrInline === 'true' || 'bsSelectedDates' in dataset || 'tblrSelectedDates' in dataset || 'bsBoundInput' in dataset || 'tblrBoundInput' in dataset || Boolean((element as HTMLInputElement).value)
|
||||||
})
|
})
|
||||||
// js-docs-end datepicker-init
|
// js-docs-end datepicker-init
|
||||||
|
|
||||||
|
|||||||
@@ -534,4 +534,99 @@ describe('Datepicker', () => {
|
|||||||
expect(input().value).toBe('not a date')
|
expect(input().value).toBe('not a date')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('format pattern and bound input', () => {
|
||||||
|
it('writes an ISO value with the iso keyword whatever the locale', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-bs-locale="pl-PL" data-bs-date-format="iso" value="2026-09-30">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input())
|
||||||
|
|
||||||
|
expect(instance._config.dateFormat).toBe('iso')
|
||||||
|
expect(input().value).toBe('2026-09-30')
|
||||||
|
expect(instance._config.locale).toBe('pl-PL')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('writes a token pattern from a data attribute', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-bs-date-format="D/M/YYYY">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input())
|
||||||
|
instance.setSelectedDates(['2026-03-04'])
|
||||||
|
|
||||||
|
expect(input().value).toBe('4/3/2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('joins a range written with a pattern', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input(), { dateFormat: 'DD.MM.YYYY', selectionMode: 'multiple-ranged' })
|
||||||
|
instance.setSelectedDates(['2026-06-10', '2026-06-18'])
|
||||||
|
|
||||||
|
expect(input().value).toBe('10.06.2026 – 18.06.2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reads a value written with the pattern back', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" value="30.09.2026">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input(), { dateFormat: 'DD.MM.YYYY' })
|
||||||
|
|
||||||
|
expect(instance.getSelectedDates()).toEqual(['2026-09-30'])
|
||||||
|
expect(input().value).toBe('30.09.2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reads a range written with the pattern back', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" value="10.06.2026 – 18.06.2026">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input(), { dateFormat: 'DD.MM.YYYY', selectionMode: 'multiple-ranged' })
|
||||||
|
|
||||||
|
expect(instance.getSelectedDates()).toEqual(['2026-06-10', '2026-06-18'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not read a day that does not exist', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" value="31.02.2026">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input(), { dateFormat: 'DD.MM.YYYY' })
|
||||||
|
|
||||||
|
expect(instance.getSelectedDates()).toEqual([])
|
||||||
|
expect(input().value).toBe('31.02.2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still reads an ISO value with another pattern', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" value="2026-09-30">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input(), { dateFormat: 'DD.MM.YYYY' })
|
||||||
|
|
||||||
|
expect(instance.getSelectedDates()).toEqual(['2026-09-30'])
|
||||||
|
expect(input().value).toBe('30.09.2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('writes ISO dates into a bound input next to a popup field', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-bs-locale="pl-PL" data-bs-bound-input="#due"><input type="hidden" id="due" name="due">'
|
||||||
|
const hidden = fixtureEl.querySelector<HTMLInputElement>('#due')!
|
||||||
|
|
||||||
|
const instance = new Datepicker(input())
|
||||||
|
instance.setSelectedDates(['2026-09-30'])
|
||||||
|
|
||||||
|
expect(input().value).toBe('30.09.2026')
|
||||||
|
expect(hidden.value).toBe('2026-09-30')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selects the dates of a bound input the server filled in', () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-bs-locale="pl-PL" data-bs-bound-input="#due"><input type="hidden" id="due" name="due" value="2026-09-30">'
|
||||||
|
|
||||||
|
const instance = new Datepicker(input())
|
||||||
|
|
||||||
|
expect(instance.getSelectedDates()).toEqual(['2026-09-30'])
|
||||||
|
expect(input().value).toBe('30.09.2026')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves a bound input outside the element in place on dispose', () => {
|
||||||
|
fixtureEl.innerHTML = '<button type="button" data-bs-bound-input="#due">Pick</button><input type="hidden" id="due" name="due">'
|
||||||
|
const hidden = fixtureEl.querySelector<HTMLInputElement>('#due')!
|
||||||
|
|
||||||
|
new Datepicker(fixtureEl.querySelector('button')!).dispose()
|
||||||
|
|
||||||
|
expect(hidden.parentElement).toBe(fixtureEl)
|
||||||
|
expect(fixtureEl.querySelector('button')!.contains(hidden)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -149,6 +149,25 @@ The popup lines up with the left edge of the field. Set `data-bs-placement` to `
|
|||||||
<input type="text" class="form-control" id="datepicker-right" data-bs-toggle="datepicker" autocomplete="off" data-bs-placement="right" placeholder="Select a date" />
|
<input type="text" class="form-control" id="datepicker-right" data-bs-toggle="datepicker" autocomplete="off" data-bs-placement="right" placeholder="Select a date" />
|
||||||
</Example>
|
</Example>
|
||||||
|
|
||||||
|
### Date format
|
||||||
|
|
||||||
|
The field shows the date in the format of `data-bs-locale`. Set `data-bs-date-format` to a pattern to write it the same way in every language. The pattern uses `YYYY` for the year, `MM` and `M` for the month, and `DD` and `D` for the day. The short tokens drop the leading zero. Everything else is copied as it is. Use `iso` as a shortcut for `YYYY-MM-DD`.
|
||||||
|
|
||||||
|
<Example>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<label class="form-label" for="datepicker-format-iso">ISO date, Polish calendar</label>
|
||||||
|
<input type="text" class="form-control" id="datepicker-format-iso" data-bs-toggle="datepicker" autocomplete="off" data-bs-locale="pl-PL" data-bs-date-format="iso" value="2026-09-30" />
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<label class="form-label" for="datepicker-format-pattern">Custom pattern</label>
|
||||||
|
<input type="text" class="form-control" id="datepicker-format-pattern" data-bs-toggle="datepicker" autocomplete="off" data-bs-date-format="DD/MM/YYYY" placeholder="DD/MM/YYYY" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Example>
|
||||||
|
|
||||||
|
A field with a pattern also reads its `value` in that pattern, so a form the server sends back keeps the picked date selected.
|
||||||
|
|
||||||
### Button trigger
|
### Button trigger
|
||||||
|
|
||||||
Any element can open the calendar, not only an input. A button shows the picked date as its text. Put the text in a child with `data-bs-datepicker-display` so an icon next to it stays.
|
Any element can open the calendar, not only an input. A button shows the picked date as its text. Put the text in a child with `data-bs-datepicker-display` so an icon next to it stays.
|
||||||
@@ -197,11 +216,22 @@ To submit the picked dates with a form, put a hidden input inside the calendar.
|
|||||||
</form>
|
</form>
|
||||||
</Example>
|
</Example>
|
||||||
|
|
||||||
|
A popup field works the same way. Point `data-bs-bound-input` at a hidden input and give the `name` to that input, not to the field. The field shows the date in the visitor's language, and the form sends `YYYY-MM-DD`. When the hidden input already has a value, the picker selects that date on page load.
|
||||||
|
|
||||||
|
<Example>
|
||||||
|
<form onsubmit="return false">
|
||||||
|
<label class="form-label" for="datepicker-bound">Due date</label>
|
||||||
|
<input type="text" class="form-control" id="datepicker-bound" data-bs-toggle="datepicker" autocomplete="off" data-bs-locale="pl-PL" data-bs-bound-input="#datepicker-bound-value" placeholder="Select a date" />
|
||||||
|
<input type="hidden" id="datepicker-bound-value" name="due_date" value="2026-09-30" />
|
||||||
|
<button type="submit" class="btn btn-primary mt-3">Submit</button>
|
||||||
|
</form>
|
||||||
|
</Example>
|
||||||
|
|
||||||
## JavaScript
|
## JavaScript
|
||||||
|
|
||||||
### Via data attributes
|
### Via data attributes
|
||||||
|
|
||||||
Add `data-bs-toggle="datepicker"` and Tabler does the rest. Inputs open on focus, other elements open on click, and inline calendars and fields with a value or `data-bs-selected-dates` render on page load. Every option below can be set as a `data-bs-*` attribute, for example `data-bs-selection-mode="multiple"`.
|
Add `data-bs-toggle="datepicker"` and Tabler does the rest. Inputs open on focus, other elements open on click, and inline calendars and fields with a value, `data-bs-selected-dates` or `data-bs-bound-input` render on page load. Every option below can be set as a `data-bs-*` attribute, for example `data-bs-selection-mode="multiple"`.
|
||||||
|
|
||||||
<CodeDocs name="datepicker-init" file="core/js/src/datepicker.ts" />
|
<CodeDocs name="datepicker-init" file="core/js/src/datepicker.ts" />
|
||||||
|
|
||||||
@@ -223,12 +253,13 @@ const datepicker = new tabler.Datepicker(document.getElementById('my-date'), {
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `dateMin` | string, number, Date | `null` | First pickable date, as `YYYY-MM-DD`. |
|
| `dateMin` | string, number, Date | `null` | First pickable date, as `YYYY-MM-DD`. |
|
||||||
| `dateMax` | string, number, Date | `null` | Last pickable date, as `YYYY-MM-DD`. |
|
| `dateMax` | string, number, Date | `null` | Last pickable date, as `YYYY-MM-DD`. |
|
||||||
| `dateFormat` | object, function | `null` | How the date is shown in the field. Pass `Intl.DateTimeFormat` options or a `function(date, locale)` that returns a string. |
|
| `boundInput` | string, element | `null` | Input that gets the picked dates as `YYYY-MM-DD`, separated by commas. An inline calendar uses a hidden input inside it. |
|
||||||
|
| `dateFormat` | string, object, function | `null` | How the date is shown in the field. Pass a pattern such as `'DD.MM.YYYY'`, `'iso'`, `Intl.DateTimeFormat` options or a `function(date, locale)` that returns a string. |
|
||||||
| `displayElement` | string, element, boolean | `null` | Element that shows the picked date. A button uses itself or its `[data-bs-datepicker-display]` child. Set `false` to show nothing. |
|
| `displayElement` | string, element, boolean | `null` | Element that shows the picked date. A button uses itself or its `[data-bs-datepicker-display]` child. Set `false` to show nothing. |
|
||||||
| `displayMonthsCount` | number | `1` | Months shown side by side. |
|
| `displayMonthsCount` | number | `1` | Months shown side by side. |
|
||||||
| `firstWeekday` | number | `1` | First day of the week: 0 is Sunday, 1 is Monday. |
|
| `firstWeekday` | number | `1` | First day of the week: 0 is Sunday, 1 is Monday. |
|
||||||
| `inline` | boolean | `false` | Render the calendar in place instead of a popup. |
|
| `inline` | boolean | `false` | Render the calendar in place instead of a popup. |
|
||||||
| `locale` | string | `'default'` | Locale for month names and the date shown in the field, for example `'en-US'` or `'de-DE'`. |
|
| `locale` | string | `'default'` | Locale for month names and the date shown in the field, for example `'en-US'` or `'de-DE'`. A `dateFormat` pattern takes over the field. |
|
||||||
| `positionElement` | string, element | `null` | Element the popup lines up with. Defaults to the `.input-icon` or `.input-group` wrapper, or the field itself. |
|
| `positionElement` | string, element | `null` | Element the popup lines up with. Defaults to the `.input-icon` or `.input-group` wrapper, or the field itself. |
|
||||||
| `selectedDates` | array | `[]` | Preselected dates as `YYYY-MM-DD`. |
|
| `selectedDates` | array | `[]` | Preselected dates as `YYYY-MM-DD`. |
|
||||||
| `selectionMode` | string | `'single'` | `'single'`, `'multiple'` or `'multiple-ranged'`. |
|
| `selectionMode` | string | `'single'` | `'single'`, `'multiple'` or `'multiple-ranged'`. |
|
||||||
@@ -298,5 +329,5 @@ Tabler 1.5 and older used [Litepicker](https://github.com/wakirin/Litepicker), w
|
|||||||
| `singleMode: false` | `data-bs-selection-mode="multiple-ranged"` |
|
| `singleMode: false` | `data-bs-selection-mode="multiple-ranged"` |
|
||||||
| `minDate` / `maxDate` | `data-bs-date-min` / `data-bs-date-max` |
|
| `minDate` / `maxDate` | `data-bs-date-min` / `data-bs-date-max` |
|
||||||
| `numberOfMonths: 2` | `data-bs-display-months-count="2"` |
|
| `numberOfMonths: 2` | `data-bs-display-months-count="2"` |
|
||||||
| `format` | `dateFormat` option, as `Intl.DateTimeFormat` options |
|
| `format: 'DD.MM.YYYY'` | `data-bs-date-format="DD.MM.YYYY"` (tokens `YYYY`, `MM`, `M`, `DD`, `D`) |
|
||||||
| `picker.on('selected', …)` | `change.bs.datepicker` event |
|
| `picker.on('selected', …)` | `change.bs.datepicker` event |
|
||||||
Reference in new issue
Block a user