This commit is contained in:
Mark Otto 2025-12-22 06:30:33 +01:00 committed by GitHub
commit ba70ea4be1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1107 additions and 2 deletions

View File

@ -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'

View File

@ -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
View 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

View 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
View File

@ -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",

View File

@ -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
View 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
View File

@ -17,6 +17,7 @@
@forward "breadcrumb";
@forward "card";
@forward "carousel";
@forward "datepicker";
@forward "dialog";
@forward "dropdown";
@forward "list-group";

View 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)
})
```