2025-06-30 10:40:23 +00:00
|
|
|
import {
|
|
|
|
customElement,
|
|
|
|
type TemplateResult,
|
|
|
|
property,
|
|
|
|
html,
|
|
|
|
css,
|
|
|
|
cssManager,
|
|
|
|
state,
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
import { DeesInputBase } from './dees-input-base.js';
|
|
|
|
import { demoFunc } from './dees-input-datepicker.demo.js';
|
|
|
|
import './dees-icon.js';
|
|
|
|
import './dees-label.js';
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
'dees-input-datepicker': DeesInputDatepicker;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@customElement('dees-input-datepicker')
|
|
|
|
export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
|
|
public static demo = demoFunc;
|
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
public value: string = '';
|
|
|
|
|
|
|
|
@property({ type: Boolean })
|
|
|
|
public enableTime: boolean = false;
|
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
public timeFormat: '24h' | '12h' = '24h';
|
|
|
|
|
|
|
|
@property({ type: Number })
|
|
|
|
public minuteIncrement: number = 1;
|
|
|
|
|
|
|
|
@property({ type: String })
|
2025-06-30 10:58:31 +00:00
|
|
|
public dateFormat: string = 'YYYY-MM-DD';
|
2025-06-30 10:40:23 +00:00
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
public minDate: string = '';
|
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
public maxDate: string = '';
|
|
|
|
|
|
|
|
@property({ type: Array })
|
|
|
|
public disabledDates: string[] = [];
|
|
|
|
|
|
|
|
@property({ type: Number })
|
|
|
|
public weekStartsOn: 0 | 1 = 1; // Default to Monday
|
|
|
|
|
|
|
|
@property({ type: String })
|
2025-06-30 10:58:31 +00:00
|
|
|
public placeholder: string = 'YYYY-MM-DD';
|
2025-06-30 10:40:23 +00:00
|
|
|
|
|
|
|
@state()
|
|
|
|
private isOpened: boolean = false;
|
|
|
|
|
|
|
|
@state()
|
|
|
|
private opensToTop: boolean = false;
|
|
|
|
|
|
|
|
@state()
|
|
|
|
private selectedDate: Date | null = null;
|
|
|
|
|
|
|
|
@state()
|
|
|
|
private viewDate: Date = new Date();
|
|
|
|
|
|
|
|
@state()
|
|
|
|
private selectedHour: number = 0;
|
|
|
|
|
|
|
|
@state()
|
|
|
|
private selectedMinute: number = 0;
|
|
|
|
|
|
|
|
public static styles = [
|
|
|
|
...DeesInputBase.baseStyles,
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
css`
|
|
|
|
:host {
|
|
|
|
display: block;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
|
|
|
|
.input-container {
|
|
|
|
position: relative;
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.date-input {
|
|
|
|
width: 100%;
|
|
|
|
height: 40px;
|
|
|
|
padding: 0 12px;
|
|
|
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
|
|
|
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
border-radius: 6px;
|
|
|
|
font-size: 14px;
|
|
|
|
line-height: 1.5;
|
|
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
|
|
cursor: pointer;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
outline: none;
|
|
|
|
font-family: inherit;
|
|
|
|
}
|
|
|
|
|
|
|
|
.date-input::placeholder {
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.date-input:hover:not(:disabled) {
|
|
|
|
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.date-input:focus,
|
|
|
|
.date-input.open {
|
|
|
|
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
|
|
|
outline: 2px solid transparent;
|
|
|
|
outline-offset: 2px;
|
|
|
|
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}, 0 0 0 4px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.date-input:disabled {
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
cursor: not-allowed;
|
|
|
|
opacity: 0.5;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Icon container using flexbox for better positioning */
|
|
|
|
.icon-container {
|
|
|
|
position: absolute;
|
|
|
|
right: 0;
|
|
|
|
top: 0;
|
|
|
|
bottom: 0;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
gap: 4px;
|
|
|
|
padding: 0 12px;
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.icon-container > * {
|
|
|
|
pointer-events: auto;
|
|
|
|
}
|
|
|
|
|
|
|
|
.calendar-icon {
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
pointer-events: none;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.clear-button {
|
|
|
|
width: 20px;
|
|
|
|
height: 20px;
|
|
|
|
border: none;
|
|
|
|
background: transparent;
|
|
|
|
cursor: pointer;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
border-radius: 4px;
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
transition: opacity 0.2s ease, background-color 0.2s ease;
|
|
|
|
padding: 0;
|
|
|
|
flex-shrink: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.clear-button:hover {
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.clear-button:disabled {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Calendar Popup Styles */
|
|
|
|
.calendar-popup {
|
|
|
|
will-change: transform, opacity;
|
|
|
|
pointer-events: none;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
opacity: 0;
|
|
|
|
transform: translateY(-4px);
|
|
|
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
|
|
|
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
box-shadow: ${cssManager.bdTheme(
|
|
|
|
'0 10px 15px -3px hsl(0 0% 0% / 0.1), 0 4px 6px -4px hsl(0 0% 0% / 0.1)',
|
|
|
|
'0 10px 15px -3px hsl(0 0% 0% / 0.2), 0 4px 6px -4px hsl(0 0% 0% / 0.2)'
|
|
|
|
)};
|
|
|
|
border-radius: 6px;
|
|
|
|
padding: 12px;
|
|
|
|
position: absolute;
|
|
|
|
user-select: none;
|
|
|
|
margin-top: 4px;
|
|
|
|
z-index: 50;
|
|
|
|
left: 0;
|
|
|
|
min-width: 280px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.calendar-popup.top {
|
|
|
|
bottom: calc(100% + 4px);
|
|
|
|
top: auto;
|
|
|
|
margin-top: 0;
|
|
|
|
margin-bottom: 4px;
|
|
|
|
transform: translateY(4px);
|
|
|
|
}
|
|
|
|
|
|
|
|
.calendar-popup.bottom {
|
|
|
|
top: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.calendar-popup.show {
|
|
|
|
pointer-events: all;
|
|
|
|
transform: translateY(0);
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Calendar Header */
|
|
|
|
.calendar-header {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: space-between;
|
|
|
|
margin-bottom: 16px;
|
|
|
|
gap: 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.month-year-display {
|
|
|
|
font-weight: 500;
|
|
|
|
font-size: 14px;
|
|
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
|
|
flex: 1;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.nav-button {
|
|
|
|
width: 28px;
|
|
|
|
height: 28px;
|
|
|
|
border: none;
|
|
|
|
background: transparent;
|
|
|
|
cursor: pointer;
|
|
|
|
border-radius: 6px;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
}
|
|
|
|
|
|
|
|
.nav-button:hover {
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.nav-button:active {
|
|
|
|
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Weekday headers */
|
|
|
|
.weekdays {
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: repeat(7, 1fr);
|
|
|
|
gap: 0;
|
|
|
|
margin-bottom: 4px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.weekday {
|
|
|
|
text-align: center;
|
|
|
|
font-size: 12px;
|
|
|
|
font-weight: 400;
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
padding: 0 0 8px 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Days grid */
|
|
|
|
.days-grid {
|
|
|
|
display: grid;
|
|
|
|
grid-template-columns: repeat(7, 1fr);
|
|
|
|
gap: 2px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.day {
|
|
|
|
aspect-ratio: 1;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
cursor: pointer;
|
|
|
|
border-radius: 6px;
|
|
|
|
font-size: 14px;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
|
|
border: none;
|
|
|
|
width: 36px;
|
|
|
|
height: 36px;
|
|
|
|
background: transparent;
|
|
|
|
}
|
|
|
|
|
|
|
|
.day:hover:not(.disabled) {
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.day.other-month {
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
opacity: 0.5;
|
|
|
|
}
|
|
|
|
|
|
|
|
.day.today {
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
font-weight: 500;
|
|
|
|
}
|
|
|
|
|
|
|
|
.day.selected {
|
|
|
|
background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
|
|
|
|
font-weight: 500;
|
|
|
|
}
|
|
|
|
|
|
|
|
.day.disabled {
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
cursor: not-allowed;
|
|
|
|
opacity: 0.3;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Time selector */
|
|
|
|
.time-selector {
|
|
|
|
margin-top: 12px;
|
|
|
|
padding-top: 12px;
|
|
|
|
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.time-selector-title {
|
|
|
|
font-size: 12px;
|
|
|
|
font-weight: 500;
|
|
|
|
margin-bottom: 8px;
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.time-inputs {
|
|
|
|
display: flex;
|
|
|
|
gap: 8px;
|
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.time-input {
|
|
|
|
width: 65px;
|
|
|
|
height: 36px;
|
|
|
|
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
border-radius: 6px;
|
|
|
|
padding: 0 12px;
|
|
|
|
font-size: 14px;
|
|
|
|
text-align: center;
|
|
|
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
}
|
|
|
|
|
|
|
|
.time-input:hover {
|
|
|
|
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.time-input:focus {
|
|
|
|
outline: none;
|
|
|
|
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
|
|
|
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.time-separator {
|
|
|
|
font-size: 14px;
|
|
|
|
font-weight: 500;
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.am-pm-selector {
|
|
|
|
display: flex;
|
|
|
|
gap: 4px;
|
|
|
|
margin-left: 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.am-pm-button {
|
|
|
|
padding: 6px 12px;
|
|
|
|
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
|
|
|
border-radius: 6px;
|
|
|
|
font-size: 12px;
|
|
|
|
font-weight: 500;
|
|
|
|
cursor: pointer;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.am-pm-button.selected {
|
|
|
|
background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
|
|
|
|
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.am-pm-button:hover:not(.selected) {
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Action buttons */
|
|
|
|
.calendar-actions {
|
|
|
|
display: flex;
|
|
|
|
gap: 8px;
|
|
|
|
margin-top: 12px;
|
|
|
|
padding-top: 12px;
|
|
|
|
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.action-button {
|
|
|
|
flex: 1;
|
|
|
|
height: 36px;
|
|
|
|
border: none;
|
|
|
|
border-radius: 6px;
|
|
|
|
font-size: 14px;
|
|
|
|
font-weight: 500;
|
|
|
|
cursor: pointer;
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
justify-content: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.today-button {
|
|
|
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
|
|
|
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.today-button:hover {
|
|
|
|
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
|
|
|
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.today-button:active {
|
|
|
|
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.clear-button {
|
|
|
|
background: transparent;
|
|
|
|
border: 1px solid transparent;
|
|
|
|
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.clear-button:hover {
|
|
|
|
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
|
|
|
|
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
|
|
|
}
|
|
|
|
|
|
|
|
.clear-button:active {
|
|
|
|
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.2)', 'hsl(0 62.8% 30.6% / 0.2)')};
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
];
|
|
|
|
|
|
|
|
render(): TemplateResult {
|
|
|
|
const monthNames = [
|
|
|
|
'January', 'February', 'March', 'April', 'May', 'June',
|
|
|
|
'July', 'August', 'September', 'October', 'November', 'December'
|
|
|
|
];
|
|
|
|
|
|
|
|
const weekDays = this.weekStartsOn === 1
|
|
|
|
? ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
|
|
|
: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
|
|
|
|
|
|
const days = this.getDaysInMonth();
|
|
|
|
const isAM = this.selectedHour < 12;
|
|
|
|
|
|
|
|
return html`
|
|
|
|
<div class="input-wrapper">
|
|
|
|
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
|
|
|
|
<div class="input-container">
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
class="date-input ${this.isOpened ? 'open' : ''}"
|
|
|
|
.value=${this.formatDate(this.value)}
|
|
|
|
.placeholder=${this.placeholder}
|
|
|
|
?disabled=${this.disabled}
|
|
|
|
@click=${this.toggleCalendar}
|
|
|
|
@keydown=${this.handleKeydown}
|
2025-06-30 10:58:31 +00:00
|
|
|
@input=${this.handleManualInput}
|
|
|
|
@blur=${this.handleInputBlur}
|
2025-06-30 10:40:23 +00:00
|
|
|
style="padding-right: ${this.value ? '64px' : '40px'}"
|
|
|
|
/>
|
|
|
|
<div class="icon-container">
|
|
|
|
${this.value && !this.disabled ? html`
|
|
|
|
<button class="clear-button" @click=${this.clearValue} title="Clear">
|
2025-06-30 11:08:14 +00:00
|
|
|
<dees-icon icon="lucide:x" iconSize="14"></dees-icon>
|
2025-06-30 10:40:23 +00:00
|
|
|
</button>
|
|
|
|
` : ''}
|
2025-06-30 11:08:14 +00:00
|
|
|
<dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
|
2025-06-30 10:40:23 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Calendar Popup -->
|
|
|
|
<div class="calendar-popup ${this.isOpened ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}">
|
|
|
|
<!-- Month/Year Navigation -->
|
|
|
|
<div class="calendar-header">
|
|
|
|
<button class="nav-button" @click="${this.previousMonth}">
|
2025-06-30 11:08:14 +00:00
|
|
|
<dees-icon icon="lucide:chevronLeft" iconSize="16"></dees-icon>
|
2025-06-30 10:40:23 +00:00
|
|
|
</button>
|
|
|
|
<div class="month-year-display">
|
|
|
|
${monthNames[this.viewDate.getMonth()]} ${this.viewDate.getFullYear()}
|
|
|
|
</div>
|
|
|
|
<button class="nav-button" @click="${this.nextMonth}">
|
2025-06-30 11:08:14 +00:00
|
|
|
<dees-icon icon="lucide:chevronRight" iconSize="16"></dees-icon>
|
2025-06-30 10:40:23 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Weekday Headers -->
|
|
|
|
<div class="weekdays">
|
|
|
|
${weekDays.map(day => html`<div class="weekday">${day}</div>`)}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Days Grid -->
|
|
|
|
<div class="days-grid">
|
|
|
|
${days.map(day => {
|
|
|
|
const isToday = this.isToday(day);
|
|
|
|
const isSelected = this.isSelected(day);
|
|
|
|
const isOtherMonth = day.getMonth() !== this.viewDate.getMonth();
|
|
|
|
const isDisabled = this.isDisabled(day);
|
|
|
|
|
|
|
|
return html`
|
|
|
|
<div
|
|
|
|
class="day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}"
|
|
|
|
@click="${() => !isDisabled && this.selectDate(day)}"
|
|
|
|
>
|
|
|
|
${day.getDate()}
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Time Selector -->
|
|
|
|
${this.enableTime ? html`
|
|
|
|
<div class="time-selector">
|
|
|
|
<div class="time-selector-title">Time</div>
|
|
|
|
<div class="time-inputs">
|
|
|
|
<input
|
|
|
|
type="number"
|
|
|
|
class="time-input"
|
|
|
|
.value="${this.timeFormat === '12h'
|
|
|
|
? (this.selectedHour === 0 ? 12 : this.selectedHour > 12 ? this.selectedHour - 12 : this.selectedHour).toString().padStart(2, '0')
|
|
|
|
: this.selectedHour.toString().padStart(2, '0')}"
|
|
|
|
@input="${(e: InputEvent) => this.handleHourInput(e)}"
|
|
|
|
min="${this.timeFormat === '12h' ? 1 : 0}"
|
|
|
|
max="${this.timeFormat === '12h' ? 12 : 23}"
|
|
|
|
/>
|
|
|
|
<span class="time-separator">:</span>
|
|
|
|
<input
|
|
|
|
type="number"
|
|
|
|
class="time-input"
|
|
|
|
.value="${this.selectedMinute.toString().padStart(2, '0')}"
|
|
|
|
@input="${(e: InputEvent) => this.handleMinuteInput(e)}"
|
|
|
|
min="0"
|
|
|
|
max="59"
|
|
|
|
step="${this.minuteIncrement || 1}"
|
|
|
|
/>
|
|
|
|
${this.timeFormat === '12h' ? html`
|
|
|
|
<div class="am-pm-selector">
|
|
|
|
<button
|
|
|
|
class="am-pm-button ${isAM ? 'selected' : ''}"
|
|
|
|
@click="${() => this.setAMPM('am')}"
|
|
|
|
>
|
|
|
|
AM
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
class="am-pm-button ${!isAM ? 'selected' : ''}"
|
|
|
|
@click="${() => this.setAMPM('pm')}"
|
|
|
|
>
|
|
|
|
PM
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
` : ''}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
<!-- Action Buttons -->
|
|
|
|
<div class="calendar-actions">
|
|
|
|
<button class="action-button today-button" @click="${this.selectToday}">
|
|
|
|
Today
|
|
|
|
</button>
|
|
|
|
<button class="action-button clear-button" @click="${this.clear}">
|
|
|
|
Clear
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
async connectedCallback() {
|
|
|
|
super.connectedCallback();
|
|
|
|
this.handleClickOutside = this.handleClickOutside.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
async disconnectedCallback() {
|
|
|
|
await super.disconnectedCallback();
|
|
|
|
document.removeEventListener('click', this.handleClickOutside);
|
|
|
|
}
|
|
|
|
|
|
|
|
async firstUpdated() {
|
|
|
|
// Initialize with empty value if not set
|
|
|
|
if (!this.value) {
|
|
|
|
this.value = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize view date and selected time
|
|
|
|
if (this.value) {
|
|
|
|
try {
|
|
|
|
const date = new Date(this.value);
|
|
|
|
if (!isNaN(date.getTime())) {
|
|
|
|
this.selectedDate = date;
|
|
|
|
this.viewDate = new Date(date);
|
|
|
|
this.selectedHour = date.getHours();
|
|
|
|
this.selectedMinute = date.getMinutes();
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
// Invalid date
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const now = new Date();
|
|
|
|
this.viewDate = new Date(now);
|
|
|
|
this.selectedHour = now.getHours();
|
|
|
|
this.selectedMinute = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:53:22 +00:00
|
|
|
public formatDate(isoString: string): string {
|
2025-06-30 10:40:23 +00:00
|
|
|
if (!isoString) return '';
|
|
|
|
|
|
|
|
try {
|
|
|
|
const date = new Date(isoString);
|
|
|
|
if (isNaN(date.getTime())) return '';
|
|
|
|
|
|
|
|
let formatted = this.dateFormat;
|
|
|
|
|
|
|
|
// Basic date formatting
|
|
|
|
const day = date.getDate().toString().padStart(2, '0');
|
|
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
|
|
const year = date.getFullYear().toString();
|
|
|
|
|
2025-06-30 10:58:31 +00:00
|
|
|
// Replace in correct order to avoid conflicts
|
2025-06-30 10:40:23 +00:00
|
|
|
formatted = formatted.replace('YYYY', year);
|
|
|
|
formatted = formatted.replace('YY', year.slice(-2));
|
2025-06-30 10:58:31 +00:00
|
|
|
formatted = formatted.replace('MM', month);
|
|
|
|
formatted = formatted.replace('DD', day);
|
2025-06-30 10:40:23 +00:00
|
|
|
|
|
|
|
// Time formatting if enabled
|
|
|
|
if (this.enableTime) {
|
|
|
|
const hours24 = date.getHours();
|
|
|
|
const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24;
|
|
|
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
|
|
const ampm = hours24 >= 12 ? 'PM' : 'AM';
|
|
|
|
|
|
|
|
if (this.timeFormat === '12h') {
|
|
|
|
formatted += ` ${hours12}:${minutes} ${ampm}`;
|
|
|
|
} else {
|
|
|
|
formatted += ` ${hours24.toString().padStart(2, '0')}:${minutes}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return formatted;
|
|
|
|
} catch {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleClickOutside = (event: MouseEvent) => {
|
|
|
|
const path = event.composedPath();
|
|
|
|
if (!path.includes(this)) {
|
|
|
|
this.isOpened = false;
|
|
|
|
document.removeEventListener('click', this.handleClickOutside);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private async toggleCalendar(): Promise<void> {
|
|
|
|
if (this.disabled) return;
|
|
|
|
|
|
|
|
this.isOpened = !this.isOpened;
|
|
|
|
|
|
|
|
if (this.isOpened) {
|
|
|
|
// Check available space and set position
|
|
|
|
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
|
|
|
const rect = inputContainer.getBoundingClientRect();
|
|
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
|
|
const spaceAbove = rect.top;
|
|
|
|
|
|
|
|
// Determine if we should open upwards (approximate height of 400px)
|
|
|
|
this.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
|
|
|
|
|
|
|
// Add click outside listener
|
|
|
|
setTimeout(() => {
|
|
|
|
document.addEventListener('click', this.handleClickOutside);
|
|
|
|
}, 0);
|
|
|
|
} else {
|
|
|
|
document.removeEventListener('click', this.handleClickOutside);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getDaysInMonth(): Date[] {
|
|
|
|
const year = this.viewDate.getFullYear();
|
|
|
|
const month = this.viewDate.getMonth();
|
|
|
|
const firstDay = new Date(year, month, 1);
|
|
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
const days: Date[] = [];
|
|
|
|
|
|
|
|
// Adjust for week start
|
|
|
|
const startOffset = this.weekStartsOn === 1
|
|
|
|
? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
|
|
|
|
: firstDay.getDay();
|
|
|
|
|
|
|
|
// Add days from previous month
|
|
|
|
for (let i = startOffset; i > 0; i--) {
|
|
|
|
days.push(new Date(year, month, 1 - i));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add days of current month
|
|
|
|
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
|
|
days.push(new Date(year, month, i));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add days from next month to complete the grid (6 rows)
|
|
|
|
const remainingDays = 42 - days.length;
|
|
|
|
for (let i = 1; i <= remainingDays; i++) {
|
|
|
|
days.push(new Date(year, month + 1, i));
|
|
|
|
}
|
|
|
|
|
|
|
|
return days;
|
|
|
|
}
|
|
|
|
|
|
|
|
private isToday(date: Date): boolean {
|
|
|
|
const today = new Date();
|
|
|
|
return date.getDate() === today.getDate() &&
|
|
|
|
date.getMonth() === today.getMonth() &&
|
|
|
|
date.getFullYear() === today.getFullYear();
|
|
|
|
}
|
|
|
|
|
|
|
|
private isSelected(date: Date): boolean {
|
|
|
|
if (!this.selectedDate) return false;
|
|
|
|
return date.getDate() === this.selectedDate.getDate() &&
|
|
|
|
date.getMonth() === this.selectedDate.getMonth() &&
|
|
|
|
date.getFullYear() === this.selectedDate.getFullYear();
|
|
|
|
}
|
|
|
|
|
|
|
|
private isDisabled(date: Date): boolean {
|
|
|
|
// Check min date
|
|
|
|
if (this.minDate) {
|
|
|
|
const min = new Date(this.minDate);
|
|
|
|
if (date < min) return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check max date
|
|
|
|
if (this.maxDate) {
|
|
|
|
const max = new Date(this.maxDate);
|
|
|
|
if (date > max) return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check disabled dates
|
|
|
|
if (this.disabledDates && this.disabledDates.length > 0) {
|
|
|
|
return this.disabledDates.some(disabledStr => {
|
|
|
|
try {
|
|
|
|
const disabled = new Date(disabledStr);
|
|
|
|
return date.getDate() === disabled.getDate() &&
|
|
|
|
date.getMonth() === disabled.getMonth() &&
|
|
|
|
date.getFullYear() === disabled.getFullYear();
|
|
|
|
} catch {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
private selectDate(date: Date): void {
|
|
|
|
this.selectedDate = new Date(
|
|
|
|
date.getFullYear(),
|
|
|
|
date.getMonth(),
|
|
|
|
date.getDate(),
|
|
|
|
this.selectedHour,
|
|
|
|
this.selectedMinute
|
|
|
|
);
|
|
|
|
|
|
|
|
this.value = this.selectedDate.toISOString();
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
|
|
|
|
if (!this.enableTime) {
|
|
|
|
this.isOpened = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private selectToday(): void {
|
|
|
|
const today = new Date();
|
|
|
|
this.selectedDate = today;
|
|
|
|
this.viewDate = new Date(today);
|
|
|
|
this.selectedHour = today.getHours();
|
|
|
|
this.selectedMinute = today.getMinutes();
|
|
|
|
|
|
|
|
this.value = this.selectedDate.toISOString();
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
|
|
|
|
if (!this.enableTime) {
|
|
|
|
this.isOpened = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private clear(): void {
|
|
|
|
this.value = '';
|
|
|
|
this.selectedDate = null;
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
this.isOpened = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
private previousMonth(): void {
|
|
|
|
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
private nextMonth(): void {
|
|
|
|
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleHourInput(e: InputEvent): void {
|
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
let value = parseInt(input.value) || 0;
|
|
|
|
|
|
|
|
if (this.timeFormat === '12h') {
|
|
|
|
value = Math.max(1, Math.min(12, value));
|
|
|
|
// Convert to 24h format
|
|
|
|
if (this.selectedHour >= 12 && value !== 12) {
|
|
|
|
this.selectedHour = value + 12;
|
|
|
|
} else if (this.selectedHour < 12 && value === 12) {
|
|
|
|
this.selectedHour = 0;
|
|
|
|
} else {
|
|
|
|
this.selectedHour = value;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.selectedHour = Math.max(0, Math.min(23, value));
|
|
|
|
}
|
|
|
|
|
|
|
|
this.updateSelectedDateTime();
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleMinuteInput(e: InputEvent): void {
|
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
let value = parseInt(input.value) || 0;
|
|
|
|
value = Math.max(0, Math.min(59, value));
|
|
|
|
|
|
|
|
if (this.minuteIncrement && this.minuteIncrement > 1) {
|
|
|
|
value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.selectedMinute = value;
|
|
|
|
this.updateSelectedDateTime();
|
|
|
|
}
|
|
|
|
|
|
|
|
private setAMPM(period: 'am' | 'pm'): void {
|
|
|
|
if (period === 'am' && this.selectedHour >= 12) {
|
|
|
|
this.selectedHour -= 12;
|
|
|
|
} else if (period === 'pm' && this.selectedHour < 12) {
|
|
|
|
this.selectedHour += 12;
|
|
|
|
}
|
|
|
|
this.updateSelectedDateTime();
|
|
|
|
}
|
|
|
|
|
|
|
|
private updateSelectedDateTime(): void {
|
|
|
|
if (this.selectedDate) {
|
|
|
|
this.selectedDate = new Date(
|
|
|
|
this.selectedDate.getFullYear(),
|
|
|
|
this.selectedDate.getMonth(),
|
|
|
|
this.selectedDate.getDate(),
|
|
|
|
this.selectedHour,
|
|
|
|
this.selectedMinute
|
|
|
|
);
|
|
|
|
this.value = this.selectedDate.toISOString();
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleKeydown(e: KeyboardEvent): void {
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
e.preventDefault();
|
|
|
|
this.toggleCalendar();
|
|
|
|
} else if (e.key === 'Escape' && this.isOpened) {
|
|
|
|
e.preventDefault();
|
|
|
|
this.isOpened = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private clearValue(e: Event): void {
|
|
|
|
e.stopPropagation();
|
|
|
|
this.value = '';
|
|
|
|
this.selectedDate = null;
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:58:31 +00:00
|
|
|
private handleManualInput(e: InputEvent): void {
|
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
const inputValue = input.value.trim();
|
|
|
|
|
|
|
|
if (!inputValue) {
|
|
|
|
// Clear the value if input is empty
|
|
|
|
this.value = '';
|
|
|
|
this.selectedDate = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const parsedDate = this.parseManualDate(inputValue);
|
|
|
|
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
|
|
|
// Update internal state without triggering re-render of input
|
|
|
|
this.value = parsedDate.toISOString();
|
|
|
|
this.selectedDate = parsedDate;
|
|
|
|
this.viewDate = new Date(parsedDate);
|
|
|
|
this.selectedHour = parsedDate.getHours();
|
|
|
|
this.selectedMinute = parsedDate.getMinutes();
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleInputBlur(e: FocusEvent): void {
|
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
const inputValue = input.value.trim();
|
|
|
|
|
|
|
|
if (!inputValue) {
|
|
|
|
this.value = '';
|
|
|
|
this.selectedDate = null;
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const parsedDate = this.parseManualDate(inputValue);
|
|
|
|
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
|
|
|
this.value = parsedDate.toISOString();
|
|
|
|
this.selectedDate = parsedDate;
|
|
|
|
this.viewDate = new Date(parsedDate);
|
|
|
|
this.selectedHour = parsedDate.getHours();
|
|
|
|
this.selectedMinute = parsedDate.getMinutes();
|
|
|
|
this.changeSubject.next(this);
|
|
|
|
// Update the input with formatted date
|
|
|
|
input.value = this.formatDate(this.value);
|
|
|
|
} else {
|
|
|
|
// Revert to previous valid value on blur if parsing failed
|
|
|
|
input.value = this.formatDate(this.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private parseManualDate(input: string): Date | null {
|
|
|
|
if (!input) return null;
|
|
|
|
|
|
|
|
// Split date and time parts if present
|
|
|
|
const parts = input.split(' ');
|
|
|
|
let datePart = parts[0];
|
|
|
|
let timePart = parts[1] || '';
|
|
|
|
|
|
|
|
let parsedDate: Date | null = null;
|
|
|
|
|
|
|
|
// Try different date formats
|
|
|
|
// Format 1: YYYY-MM-DD (ISO-like)
|
|
|
|
const isoMatch = datePart.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
|
|
if (isoMatch) {
|
|
|
|
const [_, year, month, day] = isoMatch;
|
|
|
|
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format 2: DD.MM.YYYY (European)
|
|
|
|
if (!parsedDate) {
|
|
|
|
const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
|
|
if (euMatch) {
|
|
|
|
const [_, day, month, year] = euMatch;
|
|
|
|
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Format 3: MM/DD/YYYY (US)
|
|
|
|
if (!parsedDate) {
|
|
|
|
const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
|
|
if (usMatch) {
|
|
|
|
const [_, month, day, year] = usMatch;
|
|
|
|
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no date was parsed, return null
|
|
|
|
if (!parsedDate || isNaN(parsedDate.getTime())) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse time if present (HH:MM format)
|
|
|
|
if (timePart) {
|
|
|
|
const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/);
|
|
|
|
if (timeMatch) {
|
|
|
|
const [_, hours, minutes] = timeMatch;
|
|
|
|
parsedDate.setHours(parseInt(hours));
|
|
|
|
parsedDate.setMinutes(parseInt(minutes));
|
|
|
|
}
|
|
|
|
} else if (!this.enableTime) {
|
|
|
|
// If time is not enabled and not provided, use current time
|
|
|
|
const now = new Date();
|
|
|
|
parsedDate.setHours(now.getHours());
|
|
|
|
parsedDate.setMinutes(now.getMinutes());
|
|
|
|
parsedDate.setSeconds(0);
|
|
|
|
parsedDate.setMilliseconds(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsedDate;
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:40:23 +00:00
|
|
|
public getValue(): string {
|
|
|
|
return this.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setValue(value: string): void {
|
|
|
|
this.value = value;
|
|
|
|
if (value) {
|
|
|
|
try {
|
|
|
|
const date = new Date(value);
|
|
|
|
if (!isNaN(date.getTime())) {
|
|
|
|
this.selectedDate = date;
|
|
|
|
this.viewDate = new Date(date);
|
|
|
|
this.selectedHour = date.getHours();
|
|
|
|
this.selectedMinute = date.getMinutes();
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
// Invalid date
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|