mirror of
https://github.com/twbs/bootstrap.git
synced 2025-12-28 05:33:57 +00:00
First pass at datepicker via Vanilla Calendar Pro
This commit is contained in:
parent
4b90546519
commit
d6eccc38e1
@ -9,6 +9,7 @@ export { default as Alert } from './src/alert.js'
|
||||
export { default as Button } from './src/button.js'
|
||||
export { default as Carousel } from './src/carousel.js'
|
||||
export { default as Collapse } from './src/collapse.js'
|
||||
export { default as Datepicker } from './src/datepicker.js'
|
||||
export { default as Dialog } from './src/dialog.js'
|
||||
export { default as Dropdown } from './src/dropdown.js'
|
||||
export { default as Offcanvas } from './src/offcanvas.js'
|
||||
|
||||
@ -9,6 +9,7 @@ import Alert from './src/alert.js'
|
||||
import Button from './src/button.js'
|
||||
import Carousel from './src/carousel.js'
|
||||
import Collapse from './src/collapse.js'
|
||||
import Datepicker from './src/datepicker.js'
|
||||
import Dialog from './src/dialog.js'
|
||||
import Dropdown from './src/dropdown.js'
|
||||
import Offcanvas from './src/offcanvas.js'
|
||||
@ -23,6 +24,7 @@ export default {
|
||||
Button,
|
||||
Carousel,
|
||||
Collapse,
|
||||
Datepicker,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
Offcanvas,
|
||||
|
||||
289
js/src/datepicker.js
Normal file
289
js/src/datepicker.js
Normal file
@ -0,0 +1,289 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Bootstrap datepicker.js
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { Calendar } from 'vanilla-calendar-pro'
|
||||
import BaseComponent from './base-component.js'
|
||||
import EventHandler from './dom/event-handler.js'
|
||||
import SelectorEngine from './dom/selector-engine.js'
|
||||
import { isDisabled } from './util/index.js'
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'datepicker'
|
||||
const DATA_KEY = 'bs.datepicker'
|
||||
const EVENT_KEY = `.${DATA_KEY}`
|
||||
const DATA_API_KEY = '.data-api'
|
||||
|
||||
const EVENT_CHANGE = `change${EVENT_KEY}`
|
||||
const EVENT_SHOW = `show${EVENT_KEY}`
|
||||
const EVENT_SHOWN = `shown${EVENT_KEY}`
|
||||
const EVENT_HIDE = `hide${EVENT_KEY}`
|
||||
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
|
||||
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
|
||||
const EVENT_FOCUS_DATA_API = `focus${EVENT_KEY}${DATA_API_KEY}`
|
||||
|
||||
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="datepicker"]'
|
||||
|
||||
const Default = {
|
||||
dateMin: null,
|
||||
dateMax: null,
|
||||
dateFormat: null, // Uses locale default if null
|
||||
firstWeekday: 1, // Monday
|
||||
locale: 'default',
|
||||
selectedDates: [],
|
||||
selectionMode: 'single', // 'single', 'multiple', 'multiple-ranged'
|
||||
showWeekNumbers: false,
|
||||
positionToInput: 'auto'
|
||||
}
|
||||
|
||||
const DefaultType = {
|
||||
dateMin: '(null|string|number|object)',
|
||||
dateMax: '(null|string|number|object)',
|
||||
dateFormat: '(null|object)',
|
||||
firstWeekday: 'number',
|
||||
locale: 'string',
|
||||
selectedDates: 'array',
|
||||
selectionMode: 'string',
|
||||
showWeekNumbers: 'boolean',
|
||||
positionToInput: 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class Datepicker extends BaseComponent {
|
||||
constructor(element, config) {
|
||||
super(element, config)
|
||||
|
||||
this._calendar = null
|
||||
this._isShown = false
|
||||
this._cleanupFn = null
|
||||
|
||||
this._initCalendar()
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get Default() {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType() {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
// Public
|
||||
toggle() {
|
||||
return this._isShown ? this.hide() : this.show()
|
||||
}
|
||||
|
||||
show() {
|
||||
if (isDisabled(this._element) || this._isShown) {
|
||||
return
|
||||
}
|
||||
|
||||
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW)
|
||||
if (showEvent.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
this._calendar.show()
|
||||
this._isShown = true
|
||||
|
||||
EventHandler.trigger(this._element, EVENT_SHOWN)
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this._isShown) {
|
||||
return
|
||||
}
|
||||
|
||||
const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
|
||||
if (hideEvent.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
this._calendar.hide()
|
||||
this._isShown = false
|
||||
|
||||
EventHandler.trigger(this._element, EVENT_HIDDEN)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._cleanupFn) {
|
||||
this._cleanupFn()
|
||||
}
|
||||
|
||||
this._calendar = null
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
getSelectedDates() {
|
||||
return this._calendar ? [...this._calendar.context.selectedDates] : []
|
||||
}
|
||||
|
||||
setSelectedDates(dates) {
|
||||
if (this._calendar) {
|
||||
this._calendar.set({ selectedDates: dates })
|
||||
}
|
||||
}
|
||||
|
||||
// Private
|
||||
_initCalendar() {
|
||||
const isInput = this._element.tagName === 'INPUT'
|
||||
|
||||
const calendarOptions = {
|
||||
inputMode: isInput,
|
||||
positionToInput: this._config.positionToInput,
|
||||
firstWeekday: this._config.firstWeekday,
|
||||
locale: this._config.locale,
|
||||
enableWeekNumbers: this._config.showWeekNumbers,
|
||||
selectionDatesMode: this._config.selectionMode,
|
||||
selectedDates: this._config.selectedDates,
|
||||
selectedTheme: 'system',
|
||||
themeAttrDetect: 'data-bs-theme'
|
||||
}
|
||||
|
||||
if (this._config.dateMin) {
|
||||
calendarOptions.dateMin = this._config.dateMin
|
||||
}
|
||||
|
||||
if (this._config.dateMax) {
|
||||
calendarOptions.dateMax = this._config.dateMax
|
||||
}
|
||||
|
||||
// Handle date selection
|
||||
calendarOptions.onClickDate = (self, event) => {
|
||||
const selectedDates = [...self.context.selectedDates]
|
||||
|
||||
if (isInput && selectedDates.length > 0) {
|
||||
// Format date for input
|
||||
const formattedDate = this._formatDateForInput(selectedDates)
|
||||
this._element.value = formattedDate
|
||||
}
|
||||
|
||||
EventHandler.trigger(this._element, EVENT_CHANGE, {
|
||||
dates: selectedDates,
|
||||
event
|
||||
})
|
||||
|
||||
// Auto-hide after selection in single mode
|
||||
if (this._config.selectionMode === 'single' && selectedDates.length > 0) {
|
||||
// Small delay to allow the UI to update
|
||||
setTimeout(() => this.hide(), 100)
|
||||
}
|
||||
}
|
||||
|
||||
calendarOptions.onShow = () => {
|
||||
this._isShown = true
|
||||
}
|
||||
|
||||
calendarOptions.onHide = () => {
|
||||
this._isShown = false
|
||||
}
|
||||
|
||||
this._calendar = new Calendar(this._element, calendarOptions)
|
||||
this._cleanupFn = this._calendar.init()
|
||||
|
||||
// Set initial value if input has a value
|
||||
if (isInput && this._element.value) {
|
||||
this._parseInputValue()
|
||||
}
|
||||
}
|
||||
|
||||
_formatDateForInput(dates) {
|
||||
if (dates.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (this._config.dateFormat) {
|
||||
// Custom formatting could be added here
|
||||
return dates.join(', ')
|
||||
}
|
||||
|
||||
// Default: locale-aware formatting
|
||||
const formatDate = dateStr => {
|
||||
const [year, month, day] = dateStr.split('-')
|
||||
const date = new Date(year, month - 1, day)
|
||||
return date.toLocaleDateString(this._config.locale === 'default' ? undefined : this._config.locale)
|
||||
}
|
||||
|
||||
if (dates.length === 1) {
|
||||
return formatDate(dates[0])
|
||||
}
|
||||
|
||||
return dates.map(d => formatDate(d)).join(', ')
|
||||
}
|
||||
|
||||
_parseInputValue() {
|
||||
// Try to parse the input value as a date
|
||||
const value = this._element.value.trim()
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const formatted = `${year}-${month}-${day}`
|
||||
this._calendar.set({ selectedDates: [formatted] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
|
||||
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
|
||||
// Only handle if not an input (inputs use focus)
|
||||
if (this.tagName === 'INPUT') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
Datepicker.getOrCreateInstance(this).toggle()
|
||||
})
|
||||
|
||||
EventHandler.on(document, EVENT_FOCUS_DATA_API, SELECTOR_DATA_TOGGLE, function () {
|
||||
// Handle focus for input elements
|
||||
if (this.tagName !== 'INPUT') {
|
||||
return
|
||||
}
|
||||
|
||||
Datepicker.getOrCreateInstance(this).show()
|
||||
})
|
||||
|
||||
// Close on outside click
|
||||
EventHandler.on(document, 'click', event => {
|
||||
const openDatepickers = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
|
||||
for (const element of openDatepickers) {
|
||||
const instance = Datepicker.getInstance(element)
|
||||
if (!instance || !instance._isShown) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if click is outside the element and calendar
|
||||
const calendarEl = instance._calendar?.context?.mainElement
|
||||
if (
|
||||
!element.contains(event.target) &&
|
||||
(!calendarEl || !calendarEl.contains(event.target))
|
||||
) {
|
||||
instance.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default Datepicker
|
||||
170
js/tests/visual/datepicker.html
Normal file
170
js/tests/visual/datepicker.html
Normal file
@ -0,0 +1,170 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="../../../dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<title>Datepicker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<h1>Datepicker <small class="text-body-secondary">Bootstrap Visual Test</small></h1>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Basic Input Datepicker</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="basicDatepicker" class="form-label">Select a date</label>
|
||||
<input type="text" class="form-control" id="basicDatepicker" data-bs-toggle="datepicker" placeholder="Click to select">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>With Min/Max Dates</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="minMaxDatepicker" class="form-label">Only dates in 2025</label>
|
||||
<input type="text" class="form-control" id="minMaxDatepicker" data-bs-toggle="datepicker" data-bs-date-min="2025-01-01" data-bs-date-max="2025-12-31" placeholder="Select a date in 2025">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Multiple Selection</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="multipleDatepicker" class="form-label">Select multiple dates</label>
|
||||
<input type="text" class="form-control" id="multipleDatepicker" data-bs-toggle="datepicker" data-bs-selection-mode="multiple" placeholder="Click to select multiple">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Range Selection</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="rangeDatepicker" class="form-label">Select a date range</label>
|
||||
<input type="text" class="form-control" id="rangeDatepicker" data-bs-toggle="datepicker" data-bs-selection-mode="multiple-ranged" placeholder="Select start and end">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Week Numbers</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="weekNumbersDatepicker" class="form-label">With week numbers</label>
|
||||
<input type="text" class="form-control" id="weekNumbersDatepicker" data-bs-toggle="datepicker" data-bs-show-week-numbers="true" placeholder="Select a date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Sunday First</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="sundayFirstDatepicker" class="form-label">Week starts on Sunday</label>
|
||||
<input type="text" class="form-control" id="sundayFirstDatepicker" data-bs-toggle="datepicker" data-bs-first-weekday="0" placeholder="Select a date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Dark Mode</h2>
|
||||
<div class="row mb-4" data-bs-theme="dark">
|
||||
<div class="col-md-6">
|
||||
<div class="p-4 bg-dark rounded">
|
||||
<label for="darkDatepicker" class="form-label text-light">Dark mode datepicker</label>
|
||||
<input type="text" class="form-control" id="darkDatepicker" data-bs-toggle="datepicker" placeholder="Select a date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>JavaScript Initialization</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="jsDatepicker" class="form-label">Initialized via JavaScript</label>
|
||||
<input type="text" class="form-control" id="jsDatepicker" placeholder="Click to select">
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="showBtn">Show</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="hideBtn">Hide</button>
|
||||
<button type="button" class="btn btn-info btn-sm" id="getDatesBtn">Get Dates</button>
|
||||
</div>
|
||||
<div id="selectedDatesOutput" class="mt-2 text-body-secondary"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Events Test</h2>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label for="eventsDatepicker" class="form-label">Events datepicker</label>
|
||||
<input type="text" class="form-control" id="eventsDatepicker" data-bs-toggle="datepicker" placeholder="Select a date">
|
||||
<div id="eventsLog" class="mt-2 p-2 bg-light border rounded" style="min-height: 100px; font-family: monospace; font-size: 12px; white-space: pre-wrap;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="../../../dist/js/bootstrap.bundle.js"></script>
|
||||
<script>
|
||||
/* global bootstrap: false */
|
||||
|
||||
// Initialize via JavaScript
|
||||
const jsDatepickerEl = document.getElementById('jsDatepicker')
|
||||
const jsDatepicker = new bootstrap.Datepicker(jsDatepickerEl, {
|
||||
firstWeekday: 1,
|
||||
selectedDates: ['2025-01-15']
|
||||
})
|
||||
|
||||
document.getElementById('showBtn').addEventListener('click', () => {
|
||||
jsDatepicker.show()
|
||||
})
|
||||
|
||||
document.getElementById('hideBtn').addEventListener('click', () => {
|
||||
jsDatepicker.hide()
|
||||
})
|
||||
|
||||
document.getElementById('getDatesBtn').addEventListener('click', () => {
|
||||
const dates = jsDatepicker.getSelectedDates()
|
||||
document.getElementById('selectedDatesOutput').textContent = `Selected: ${dates.length ? dates.join(', ') : 'None'}`
|
||||
})
|
||||
|
||||
// Events test
|
||||
const eventsDatepickerEl = document.getElementById('eventsDatepicker')
|
||||
const eventsLog = document.getElementById('eventsLog')
|
||||
|
||||
function logEvent(eventName, detail) {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
const detailStr = detail ? ` - ${JSON.stringify(detail)}` : ''
|
||||
eventsLog.textContent += `[${timestamp}] ${eventName}${detailStr}\n`
|
||||
eventsLog.scrollTop = eventsLog.scrollHeight
|
||||
}
|
||||
|
||||
eventsDatepickerEl.addEventListener('show.bs.datepicker', () => {
|
||||
logEvent('show.bs.datepicker')
|
||||
})
|
||||
|
||||
eventsDatepickerEl.addEventListener('shown.bs.datepicker', () => {
|
||||
logEvent('shown.bs.datepicker')
|
||||
})
|
||||
|
||||
eventsDatepickerEl.addEventListener('hide.bs.datepicker', () => {
|
||||
logEvent('hide.bs.datepicker')
|
||||
})
|
||||
|
||||
eventsDatepickerEl.addEventListener('hidden.bs.datepicker', () => {
|
||||
logEvent('hidden.bs.datepicker')
|
||||
})
|
||||
|
||||
eventsDatepickerEl.addEventListener('change.bs.datepicker', event => {
|
||||
logEvent('change.bs.datepicker', { dates: event.dates })
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -19,7 +19,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-prefix-custom-properties": "^0.1.0"
|
||||
"postcss-prefix-custom-properties": "^0.1.0",
|
||||
"vanilla-calendar-pro": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.5",
|
||||
@ -19737,6 +19738,16 @@
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vanilla-calendar-pro": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vanilla-calendar-pro/-/vanilla-calendar-pro-3.0.5.tgz",
|
||||
"integrity": "sha512-4X9bmTo1/KzbZrB7B6mZXtvVXIhcKxaVSnFZuaVtps7tshKJDxgaIElkgdia6IjB5qWetWuu7kZ+ZaV1sPxy6w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://buymeacoffee.com/uvarov"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@ -210,6 +210,7 @@
|
||||
"volar-service-emmet": "0.0.63"
|
||||
},
|
||||
"dependencies": {
|
||||
"postcss-prefix-custom-properties": "^0.1.0"
|
||||
"postcss-prefix-custom-properties": "^0.1.0",
|
||||
"vanilla-calendar-pro": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
476
scss/_datepicker.scss
Normal file
476
scss/_datepicker.scss
Normal file
@ -0,0 +1,476 @@
|
||||
// Datepicker
|
||||
//
|
||||
// Bootstrap wrapper for vanilla-calendar-pro
|
||||
// Base styles from vanilla-calendar-pro v3.0.5, adapted for Bootstrap
|
||||
|
||||
@use "config" as *;
|
||||
@use "colors" as *;
|
||||
@use "variables" as *;
|
||||
|
||||
// scss-docs-start datepicker-variables
|
||||
$datepicker-padding: 1rem !default;
|
||||
$datepicker-bg: var(--bg-body) !default;
|
||||
$datepicker-color: var(--color-body) !default;
|
||||
$datepicker-border-color: var(--border-color) !default;
|
||||
$datepicker-border-width: var(--border-width) !default;
|
||||
$datepicker-border-radius: var(--border-radius-lg) !default;
|
||||
$datepicker-box-shadow: var(--box-shadow) !default;
|
||||
$datepicker-font-size: var(--font-size-sm) !default;
|
||||
$datepicker-min-width: 272px !default;
|
||||
|
||||
$datepicker-header-font-weight: 700 !default;
|
||||
$datepicker-weekday-color: var(--secondary-text) !default;
|
||||
$datepicker-day-hover-bg: var(--tertiary-bg) !default;
|
||||
$datepicker-day-selected-bg: var(--primary-bg) !default;
|
||||
$datepicker-day-selected-color: var(--primary-contrast) !default;
|
||||
$datepicker-day-today-bg: var(--secondary-bg) !default;
|
||||
$datepicker-day-today-color: var(--primary-text) !default;
|
||||
$datepicker-day-disabled-color: var(--tertiary-text) !default;
|
||||
// scss-docs-end datepicker-variables
|
||||
|
||||
@layer components {
|
||||
// scss-docs-start datepicker-css-vars
|
||||
[data-vc="calendar"] {
|
||||
--datepicker-padding: #{$datepicker-padding};
|
||||
--datepicker-bg: #{$datepicker-bg};
|
||||
--datepicker-color: #{$datepicker-color};
|
||||
--datepicker-border-color: #{$datepicker-border-color};
|
||||
--datepicker-border-width: #{$datepicker-border-width};
|
||||
--datepicker-border-radius: #{$datepicker-border-radius};
|
||||
--datepicker-box-shadow: #{$datepicker-box-shadow};
|
||||
--datepicker-font-size: #{$datepicker-font-size};
|
||||
--datepicker-min-width: #{$datepicker-min-width};
|
||||
--datepicker-zindex: #{$zindex-dropdown};
|
||||
|
||||
--datepicker-header-font-weight: #{$datepicker-header-font-weight};
|
||||
--datepicker-weekday-color: #{$datepicker-weekday-color};
|
||||
--datepicker-day-hover-bg: #{$datepicker-day-hover-bg};
|
||||
--datepicker-day-selected-bg: #{$datepicker-day-selected-bg};
|
||||
--datepicker-day-selected-color: #{$datepicker-day-selected-color};
|
||||
--datepicker-day-today-bg: #{$datepicker-day-today-bg};
|
||||
--datepicker-day-today-color: #{$datepicker-day-today-color};
|
||||
--datepicker-day-disabled-color: #{$datepicker-day-disabled-color};
|
||||
// scss-docs-end datepicker-css-vars
|
||||
|
||||
border-radius: var(--datepicker-border-radius);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: var(--datepicker-min-width);
|
||||
opacity: 1;
|
||||
padding: var(--datepicker-padding);
|
||||
position: relative;
|
||||
transition: opacity .15s ease-in-out;
|
||||
font-family: var(--font-sans-serif);
|
||||
font-size: var(--datepicker-font-size);
|
||||
background-color: var(--datepicker-bg);
|
||||
color: var(--datepicker-color);
|
||||
border: var(--datepicker-border-width) solid var(--datepicker-border-color);
|
||||
box-shadow: var(--datepicker-box-shadow);
|
||||
z-index: var(--datepicker-zindex);
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
[data-vc="calendar"]:focus-visible,
|
||||
[data-vc="calendar"] button:focus-visible,
|
||||
[data-vc="calendar"] [tabindex="0"]:focus-visible {
|
||||
border-radius: $border-radius;
|
||||
outline: 0;
|
||||
box-shadow: $focus-ring-box-shadow;
|
||||
}
|
||||
|
||||
[data-vc="calendar"][data-vc-calendar-hidden] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-vc="calendar"][data-vc-input] {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
[data-vc="calendar"][data-vc-input][data-vc-position="bottom"] {
|
||||
margin-top: .25rem;
|
||||
}
|
||||
|
||||
[data-vc="calendar"][data-vc-input][data-vc-position="top"] {
|
||||
margin-top: -.25rem;
|
||||
}
|
||||
|
||||
// Controls (arrows)
|
||||
[data-vc="controls"] {
|
||||
align-items: center;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
left: 0;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 1.25rem;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
[data-vc-arrow] {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 1.5rem;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
width: 1.5rem;
|
||||
border-radius: $border-radius;
|
||||
color: var(--datepicker-color);
|
||||
|
||||
&::before {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
content: "";
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%236b7280' d='M12 16c-.3 0-.5-.1-.7-.3l-6-6c-.4-.4-.4-1 0-1.4s1-.4 1.4 0l5.3 5.3 5.3-5.3c.4-.4 1-.4 1.4 0s.4 1 0 1.4l-6 6c-.2.2-.4.3-.7.3'/></svg>");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--datepicker-day-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
[data-vc-arrow="prev"]::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
[data-vc-arrow="next"]::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
// Grid layout
|
||||
[data-vc="grid"] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.75rem;
|
||||
}
|
||||
|
||||
[data-vc="column"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
// Header
|
||||
[data-vc="header"] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: .75rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-vc-header="content"] {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
grid-auto-columns: max-content;
|
||||
grid-auto-flow: column;
|
||||
justify-content: center;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
[data-vc="month"],
|
||||
[data-vc="year"] {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: $border-radius;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: var(--datepicker-header-font-weight);
|
||||
line-height: 1.5rem;
|
||||
padding: .25rem;
|
||||
color: var(--datepicker-color);
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
color: var(--datepicker-day-disabled-color);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--datepicker-day-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
// Content wrapper
|
||||
[data-vc="content"],
|
||||
[data-vc="wrapper"] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
[data-vc="content"] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Month/Year grids
|
||||
[data-vc="months"] {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
[data-vc="months"],
|
||||
[data-vc="years"] {
|
||||
align-items: center;
|
||||
column-gap: .25rem;
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
[data-vc="years"] {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
[data-vc-months-month],
|
||||
[data-vc-years-year] {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: $border-radius;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
height: 2.5rem;
|
||||
justify-content: center;
|
||||
line-height: 1rem;
|
||||
padding: .25rem;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
color: var(--datepicker-weekday-color);
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
color: var(--datepicker-day-disabled-color);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--datepicker-day-hover-bg);
|
||||
}
|
||||
|
||||
&[data-vc-months-month-selected],
|
||||
&[data-vc-years-year-selected] {
|
||||
background-color: var(--datepicker-day-selected-bg);
|
||||
color: var(--datepicker-day-selected-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--datepicker-day-selected-bg);
|
||||
color: var(--datepicker-day-selected-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Week numbers
|
||||
[data-vc-week="numbers"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-vc-week-numbers="title"] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: .75rem;
|
||||
font-weight: 700;
|
||||
justify-content: center;
|
||||
line-height: 1rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
[data-vc-week-numbers="content"] {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
justify-items: center;
|
||||
row-gap: .25rem;
|
||||
}
|
||||
|
||||
[data-vc-week-number] {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
line-height: 1rem;
|
||||
margin: 0;
|
||||
min-height: 1.875rem;
|
||||
min-width: 1.875rem;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
color: var(--datepicker-weekday-color);
|
||||
}
|
||||
|
||||
// Week days header
|
||||
[data-vc="week"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
justify-items: center;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
[data-vc-week-day] {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
display: flex;
|
||||
font-size: .75rem;
|
||||
font-weight: 700;
|
||||
justify-content: center;
|
||||
line-height: 1rem;
|
||||
margin: 0;
|
||||
min-width: 1.875rem;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
color: var(--datepicker-weekday-color);
|
||||
}
|
||||
|
||||
button[data-vc-week-day] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Dates grid
|
||||
[data-vc="dates"] {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
justify-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-vc-date] {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: .125rem;
|
||||
padding-top: .125rem;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-vc-date]:not(:has([data-vc-date-btn])),
|
||||
[data-vc-date][data-vc-date-disabled],
|
||||
[data-vc-date][data-vc-date-disabled] [data-vc-date-btn] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Date button
|
||||
[data-vc-date-btn] {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: $border-radius;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: .75rem;
|
||||
font-weight: 400;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
line-height: 1rem;
|
||||
min-height: 1.875rem;
|
||||
min-width: 1.875rem;
|
||||
padding: 0;
|
||||
transition: background-color .15s ease-in-out, color .15s ease-in-out;
|
||||
width: 100%;
|
||||
color: var(--datepicker-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--datepicker-day-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
// Today
|
||||
[data-vc-date][data-vc-date-today] [data-vc-date-btn] {
|
||||
font-weight: 700;
|
||||
background-color: var(--datepicker-day-today-bg);
|
||||
color: var(--datepicker-day-today-color);
|
||||
}
|
||||
|
||||
// Selected
|
||||
[data-vc-date][data-vc-date-selected] [data-vc-date-btn] {
|
||||
background-color: var(--datepicker-day-selected-bg);
|
||||
color: var(--datepicker-day-selected-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--datepicker-day-selected-bg);
|
||||
color: var(--datepicker-day-selected-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Outside month
|
||||
[data-vc-date][data-vc-date-month="next"] [data-vc-date-btn],
|
||||
[data-vc-date][data-vc-date-month="prev"] [data-vc-date-btn] {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
// Disabled
|
||||
[data-vc-date][data-vc-date-disabled] [data-vc-date-btn] {
|
||||
color: var(--datepicker-day-disabled-color);
|
||||
}
|
||||
|
||||
// Range selection styles
|
||||
[data-vc-date][data-vc-date-hover] [data-vc-date-btn] {
|
||||
border-radius: 0;
|
||||
background-color: var(--datepicker-day-hover-bg);
|
||||
}
|
||||
|
||||
[data-vc-date][data-vc-date-hover="first"] [data-vc-date-btn] {
|
||||
border-top-left-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
}
|
||||
|
||||
[data-vc-date][data-vc-date-hover="last"] [data-vc-date-btn] {
|
||||
border-top-right-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
}
|
||||
|
||||
[data-vc-date][data-vc-date-hover="first-and-last"] [data-vc-date-btn] {
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
[data-vc-date][data-vc-date-selected="middle"] [data-vc-date-btn] {
|
||||
border-radius: 0;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
[data-vc-date][data-vc-date-selected="first"] [data-vc-date-btn] {
|
||||
border-top-left-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
[data-vc-date][data-vc-date-selected="last"] [data-vc-date-btn] {
|
||||
border-top-right-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
[data-vc-date][data-vc-date-selected="first-and-last"] [data-vc-date-btn] {
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
1
scss/bootstrap.scss
vendored
1
scss/bootstrap.scss
vendored
@ -17,6 +17,7 @@
|
||||
@forward "breadcrumb";
|
||||
@forward "card";
|
||||
@forward "carousel";
|
||||
@forward "datepicker";
|
||||
@forward "dialog";
|
||||
@forward "dropdown";
|
||||
@forward "list-group";
|
||||
|
||||
154
site/src/content/docs/components/datepicker.mdx
Normal file
154
site/src/content/docs/components/datepicker.mdx
Normal file
@ -0,0 +1,154 @@
|
||||
---
|
||||
title: Datepicker
|
||||
description: A flexible date picker component powered by vanilla-calendar-pro, with Bootstrap styling and data attribute support.
|
||||
toc: true
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Bootstrap Datepicker is a wrapper around [vanilla-calendar-pro](https://vanilla-calendar.pro/) that provides a consistent, accessible date selection experience. It supports light/dark themes, input binding, and flexible configuration via data attributes or JavaScript.
|
||||
|
||||
<Example code={`<input type="text" class="form-control" data-bs-toggle="datepicker" placeholder="Select a date">`} />
|
||||
|
||||
## How it works
|
||||
|
||||
- Add `data-bs-toggle="datepicker"` to any `<input>` element to enable the datepicker
|
||||
- When focused, the calendar popup appears below the input
|
||||
- Selecting a date updates the input value and closes the picker
|
||||
- The picker respects Bootstrap's color modes (`data-bs-theme`)
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic input datepicker
|
||||
|
||||
The simplest use case: add the data attribute to a text input.
|
||||
|
||||
<Example code={`<div class="mb-3">
|
||||
<label for="datepicker1" class="form-label">Select date</label>
|
||||
<input type="text" class="form-control" id="datepicker1" data-bs-toggle="datepicker" placeholder="Click to select a date">
|
||||
</div>`} />
|
||||
|
||||
### With min and max dates
|
||||
|
||||
Restrict the selectable date range using `data-bs-date-min` and `data-bs-date-max`.
|
||||
|
||||
<Example code={`<div class="mb-3">
|
||||
<label for="datepicker2" class="form-label">Event date (2025 only)</label>
|
||||
<input type="text" class="form-control" id="datepicker2" data-bs-toggle="datepicker" data-bs-date-min="2025-01-01" data-bs-date-max="2025-12-31" placeholder="Select a date in 2025">
|
||||
</div>`} />
|
||||
|
||||
### Multiple date selection
|
||||
|
||||
Enable multiple date selection with `data-bs-selection-mode="multiple"`.
|
||||
|
||||
<Example code={`<div class="mb-3">
|
||||
<label for="datepicker3" class="form-label">Select multiple dates</label>
|
||||
<input type="text" class="form-control" id="datepicker3" data-bs-toggle="datepicker" data-bs-selection-mode="multiple" placeholder="Click to select dates">
|
||||
</div>`} />
|
||||
|
||||
### Date range selection
|
||||
|
||||
Select a range of dates with `data-bs-selection-mode="multiple-ranged"`.
|
||||
|
||||
<Example code={`<div class="mb-3">
|
||||
<label for="datepicker4" class="form-label">Select date range</label>
|
||||
<input type="text" class="form-control" id="datepicker4" data-bs-toggle="datepicker" data-bs-selection-mode="multiple-ranged" placeholder="Select start and end dates">
|
||||
</div>`} />
|
||||
|
||||
### Week numbers
|
||||
|
||||
Show week numbers alongside dates with `data-bs-show-week-numbers="true"`.
|
||||
|
||||
<Example code={`<div class="mb-3">
|
||||
<label for="datepicker5" class="form-label">With week numbers</label>
|
||||
<input type="text" class="form-control" id="datepicker5" data-bs-toggle="datepicker" data-bs-show-week-numbers="true" placeholder="Select a date">
|
||||
</div>`} />
|
||||
|
||||
### First day of week
|
||||
|
||||
Set the first day of the week (0 = Sunday, 1 = Monday, etc.) with `data-bs-first-weekday`.
|
||||
|
||||
<Example code={`<div class="mb-3">
|
||||
<label for="datepicker6" class="form-label">Week starts on Sunday</label>
|
||||
<input type="text" class="form-control" id="datepicker6" data-bs-toggle="datepicker" data-bs-first-weekday="0" placeholder="Select a date">
|
||||
</div>`} />
|
||||
|
||||
## Dark mode
|
||||
|
||||
The datepicker automatically adapts to Bootstrap's color modes. When `data-bs-theme="dark"` is set on a parent element or the `<html>` tag, the picker displays in dark mode.
|
||||
|
||||
<Example code={`<div data-bs-theme="dark" class="p-3 bg-dark rounded">
|
||||
<label for="datepickerDark" class="form-label text-light">Dark mode datepicker</label>
|
||||
<input type="text" class="form-control" id="datepickerDark" data-bs-toggle="datepicker" placeholder="Select a date">
|
||||
</div>`} />
|
||||
|
||||
## Usage
|
||||
|
||||
### Via data attributes
|
||||
|
||||
Add `data-bs-toggle="datepicker"` to any input element to initialize it as a datepicker.
|
||||
|
||||
```html
|
||||
<input type="text" class="form-control" data-bs-toggle="datepicker">
|
||||
```
|
||||
|
||||
### Via JavaScript
|
||||
|
||||
Initialize datepickers programmatically:
|
||||
|
||||
```js
|
||||
const datepickerEl = document.getElementById('myDatepicker')
|
||||
const datepicker = new bootstrap.Datepicker(datepickerEl, {
|
||||
selectionMode: 'single',
|
||||
firstWeekday: 1
|
||||
})
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
<BsTable>
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `dateMin` | string, number, Date | `null` | Minimum selectable date. Format: `YYYY-MM-DD` |
|
||||
| `dateMax` | string, number, Date | `null` | Maximum selectable date. Format: `YYYY-MM-DD` |
|
||||
| `firstWeekday` | number | `1` | First day of week (0 = Sunday, 1 = Monday, etc.) |
|
||||
| `locale` | string | `'default'` | Locale for date formatting (e.g., `'en-US'`, `'de-DE'`) |
|
||||
| `selectedDates` | array | `[]` | Pre-selected dates in `YYYY-MM-DD` format |
|
||||
| `selectionMode` | string | `'single'` | Selection mode: `'single'`, `'multiple'`, or `'multiple-ranged'` |
|
||||
| `showWeekNumbers` | boolean | `false` | Whether to show week numbers |
|
||||
| `positionToInput` | string | `'auto'` | Calendar position relative to input: `'auto'`, `'center'`, `'left'`, `'right'` |
|
||||
</BsTable>
|
||||
|
||||
### Methods
|
||||
|
||||
<BsTable>
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `show()` | Shows the datepicker calendar |
|
||||
| `hide()` | Hides the datepicker calendar |
|
||||
| `toggle()` | Toggles the datepicker visibility |
|
||||
| `getSelectedDates()` | Returns an array of selected dates in `YYYY-MM-DD` format |
|
||||
| `setSelectedDates(dates)` | Sets the selected dates. Expects an array of `YYYY-MM-DD` strings |
|
||||
| `dispose()` | Destroys the datepicker instance |
|
||||
| `getInstance(element)` | Static method to get the datepicker instance from a DOM element |
|
||||
| `getOrCreateInstance(element)` | Static method to get or create a datepicker instance |
|
||||
</BsTable>
|
||||
|
||||
### Events
|
||||
|
||||
<BsTable>
|
||||
| Event | Description |
|
||||
| --- | --- |
|
||||
| `show.bs.datepicker` | Fires immediately when the `show` method is called |
|
||||
| `shown.bs.datepicker` | Fires when the datepicker has been made visible |
|
||||
| `hide.bs.datepicker` | Fires immediately when the `hide` method is called |
|
||||
| `hidden.bs.datepicker` | Fires when the datepicker has been hidden |
|
||||
| `change.bs.datepicker` | Fires when a date is selected. Event includes `dates` (array) and `event` properties |
|
||||
</BsTable>
|
||||
|
||||
```js
|
||||
const datepickerEl = document.getElementById('myDatepicker')
|
||||
datepickerEl.addEventListener('change.bs.datepicker', event => {
|
||||
console.log('Selected dates:', event.dates)
|
||||
})
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user