Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 931797466a | |||
| 9bfb6446af | |||
| 976039798a | |||
| 0e2176ec7d |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-05 - 3.59.0 - feat(input)
|
||||
extract datepicker popup into a window-layer overlay and enhance the code editor modal status UI
|
||||
|
||||
- move the datepicker calendar, time, timezone, and event rendering into a dedicated popup component exported from the input module
|
||||
- render the datepicker popup in a window-layer overlay with reposition and cleanup handling for scroll, resize, and close events
|
||||
- preserve timezone-aware value formatting for selected dates
|
||||
- add a footer to the code editor modal showing cursor position, line count, and selected language
|
||||
- apply modal-specific Monaco background themes that react to light and dark mode
|
||||
|
||||
## 2026-04-05 - 3.58.0 - feat(dees-input-code)
|
||||
add editor status footer with cursor position, line count, and language display
|
||||
|
||||
- Tracks and displays the current cursor line and column in the code editor footer
|
||||
- Shows dynamic line count updates as editor content changes
|
||||
- Aligns the Monaco editor background with the surrounding tile theme, including light and dark mode updates
|
||||
|
||||
## 2026-04-05 - 3.57.0 - feat(dees-input-fileupload)
|
||||
redesign the file upload dropzone with dees-tile integration and themed file list styling
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "3.57.0",
|
||||
"version": "3.59.0",
|
||||
"private": false,
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.57.0',
|
||||
version: '3.59.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
@@ -79,6 +79,12 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
@state()
|
||||
accessor copySuccess: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor lineCount: number = 0;
|
||||
|
||||
@state()
|
||||
accessor cursorPosition: { line: number; column: number } = { line: 1, column: 1 };
|
||||
|
||||
private editorElement: DeesWorkspaceMonaco | null = null;
|
||||
|
||||
public static styles = [
|
||||
@@ -223,6 +229,11 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Match Monaco background to tile */
|
||||
dees-workspace-monaco::part(container) {
|
||||
background: var(--dees-color-bg-primary);
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
@@ -230,6 +241,32 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
color: var(--dees-color-text-muted);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
:host([disabled]) .code-container {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@@ -314,6 +351,16 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
@content-change=${this.handleContentChange}
|
||||
></dees-workspace-monaco>
|
||||
</div>
|
||||
<div slot="footer" class="editor-footer">
|
||||
<div class="footer-left">
|
||||
<span>Ln ${this.cursorPosition.line}, Col ${this.cursorPosition.column}</span>
|
||||
<div class="footer-separator"></div>
|
||||
<span>${this.lineCount} line${this.lineCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span>${(LANGUAGES.find(l => l.key === this.language) || LANGUAGES[0]).label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</dees-tile>
|
||||
</div>
|
||||
`;
|
||||
@@ -329,6 +376,49 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
this.changeSubject.next(this as any);
|
||||
}
|
||||
});
|
||||
|
||||
// Track cursor position and line count
|
||||
const editor = await this.editorElement.editorDeferred.promise;
|
||||
|
||||
// Set initial line count
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
this.lineCount = model.getLineCount();
|
||||
model.onDidChangeContent(() => {
|
||||
this.lineCount = model.getLineCount();
|
||||
});
|
||||
}
|
||||
|
||||
// Track cursor position
|
||||
editor.onDidChangeCursorPosition((e) => {
|
||||
this.cursorPosition = { line: e.position.lineNumber, column: e.position.column };
|
||||
});
|
||||
|
||||
// Override Monaco editor background to match tile
|
||||
const domtoolsInstance = await this.editorElement.domtoolsPromise;
|
||||
const updateEditorBg = (isBright: boolean) => {
|
||||
const bg = isBright ? '#ffffff' : '#0a0a0a';
|
||||
editor.updateOptions({});
|
||||
// Override via Monaco's theme API
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-light', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
editor.updateOptions({ theme: isBright ? 'dees-light' : 'dees-dark' });
|
||||
};
|
||||
|
||||
updateEditorBg(domtoolsInstance.themeManager.goBrightBoolean);
|
||||
domtoolsInstance.themeManager.themeObservable.subscribe((goBright: boolean) => {
|
||||
updateEditorBg(goBright);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,23 +508,15 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
||||
if (!toolbar) return;
|
||||
|
||||
// Update language button text
|
||||
const langBtn = toolbar.querySelector('.language-button span');
|
||||
if (langBtn) langBtn.textContent = getLanguageLabel();
|
||||
|
||||
// Update word wrap button
|
||||
const wrapBtn = toolbar.querySelector('.wrap-btn') as HTMLElement;
|
||||
if (wrapBtn) {
|
||||
wrapBtn.classList.toggle('active', modalWordWrap === 'on');
|
||||
}
|
||||
if (wrapBtn) wrapBtn.classList.toggle('active', modalWordWrap === 'on');
|
||||
|
||||
// Update line numbers button
|
||||
const linesBtn = toolbar.querySelector('.lines-btn') as HTMLElement;
|
||||
if (linesBtn) {
|
||||
linesBtn.classList.toggle('active', modalShowLineNumbers);
|
||||
}
|
||||
if (linesBtn) linesBtn.classList.toggle('active', modalShowLineNumbers);
|
||||
|
||||
// Update copy button
|
||||
const copyBtn = toolbar.querySelector('.copy-btn') as HTMLElement;
|
||||
const copyIcon = copyBtn?.querySelector('dees-icon') as any;
|
||||
if (copyBtn && copyIcon) {
|
||||
@@ -442,13 +524,28 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
copyIcon.icon = modalCopySuccess ? 'lucide:Check' : 'lucide:Copy';
|
||||
}
|
||||
|
||||
// Update dropdown visibility
|
||||
const dropdown = toolbar.querySelector('.language-dropdown') as HTMLElement;
|
||||
if (dropdown) {
|
||||
dropdown.style.display = modalLanguageDropdownOpen ? 'block' : 'none';
|
||||
}
|
||||
if (dropdown) dropdown.style.display = modalLanguageDropdownOpen ? 'block' : 'none';
|
||||
};
|
||||
|
||||
// Helper to update footer UI
|
||||
const updateFooterUI = (modal: DeesModal) => {
|
||||
const footer = modal.shadowRoot?.querySelector('.modal-footer');
|
||||
if (!footer) return;
|
||||
|
||||
const cursorEl = footer.querySelector('.footer-cursor');
|
||||
const linesEl = footer.querySelector('.footer-lines');
|
||||
const langEl = footer.querySelector('.footer-lang');
|
||||
|
||||
if (cursorEl) cursorEl.textContent = `Ln ${modalCursorLine}, Col ${modalCursorCol}`;
|
||||
if (linesEl) linesEl.textContent = `${modalLineCount} line${modalLineCount !== 1 ? 's' : ''}`;
|
||||
if (langEl) langEl.textContent = getLanguageLabel();
|
||||
};
|
||||
|
||||
let modalCursorLine = 1;
|
||||
let modalCursorCol = 1;
|
||||
let modalLineCount = currentValue.split('\n').length;
|
||||
|
||||
const modal = await DeesModal.createAndShow({
|
||||
heading: this.label || 'Code Editor',
|
||||
width: 'fullscreen',
|
||||
@@ -459,9 +556,7 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
padding: 4px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
.modal-toolbar .toolbar-left {
|
||||
@@ -554,9 +649,30 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
}
|
||||
.modal-editor-wrapper {
|
||||
position: relative;
|
||||
height: calc(100vh - 175px);
|
||||
height: calc(100vh - 200px);
|
||||
width: 100%;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 55%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
.modal-footer .footer-left,
|
||||
.modal-footer .footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.modal-footer .footer-separator {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
|
||||
}
|
||||
</style>
|
||||
<div class="modal-toolbar">
|
||||
<div class="toolbar-left">
|
||||
@@ -597,6 +713,16 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
.wordWrap=${modalWordWrap}
|
||||
></dees-workspace-monaco>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="footer-left">
|
||||
<span class="footer-cursor">Ln ${modalCursorLine}, Col ${modalCursorCol}</span>
|
||||
<div class="footer-separator"></div>
|
||||
<span class="footer-lines">${modalLineCount} line${modalLineCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span class="footer-lang">${getLanguageLabel()}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
@@ -608,7 +734,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
{
|
||||
name: 'Save & Close',
|
||||
action: async (modalRef) => {
|
||||
// Get the editor content from the modal
|
||||
modalEditorElement = modalRef!.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
||||
if (modalEditorElement) {
|
||||
const editor = await modalEditorElement.editorDeferred.promise;
|
||||
@@ -625,17 +750,61 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
modalEditorElement = modal.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
||||
|
||||
// Apply custom Monaco theme for matching background
|
||||
if (modalEditorElement) {
|
||||
const editor = await modalEditorElement.editorDeferred.promise;
|
||||
const domtoolsInstance = await modalEditorElement.domtoolsPromise;
|
||||
|
||||
const applyModalTheme = (isBright: boolean) => {
|
||||
const bg = isBright ? '#ffffff' : '#0a0a0a';
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-light', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
editor.updateOptions({ theme: isBright ? 'dees-light' : 'dees-dark' });
|
||||
};
|
||||
|
||||
applyModalTheme(domtoolsInstance.themeManager.goBrightBoolean);
|
||||
domtoolsInstance.themeManager.themeObservable.subscribe((goBright: boolean) => {
|
||||
applyModalTheme(goBright);
|
||||
});
|
||||
|
||||
// Track cursor position
|
||||
editor.onDidChangeCursorPosition((e) => {
|
||||
modalCursorLine = e.position.lineNumber;
|
||||
modalCursorCol = e.position.column;
|
||||
updateFooterUI(modal);
|
||||
});
|
||||
|
||||
// Track line count
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
modalLineCount = model.getLineCount();
|
||||
updateFooterUI(modal);
|
||||
model.onDidChangeContent(() => {
|
||||
modalLineCount = model.getLineCount();
|
||||
updateFooterUI(modal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up toolbar event handlers
|
||||
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
||||
if (toolbar) {
|
||||
// Language button click
|
||||
const langBtn = toolbar.querySelector('.language-button');
|
||||
langBtn?.addEventListener('click', () => {
|
||||
modalLanguageDropdownOpen = !modalLanguageDropdownOpen;
|
||||
updateToolbarUI(modal);
|
||||
});
|
||||
|
||||
// Language option clicks
|
||||
const langOptions = toolbar.querySelectorAll('.language-option');
|
||||
langOptions.forEach((option) => {
|
||||
option.addEventListener('click', async () => {
|
||||
@@ -644,23 +813,21 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
modalLanguage = newLang;
|
||||
modalLanguageDropdownOpen = false;
|
||||
|
||||
// Update editor language
|
||||
const editor = await modalEditorElement.editorDeferred.promise;
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
(window as any).monaco.editor.setModelLanguage(model, newLang);
|
||||
const editorModel = editor.getModel();
|
||||
if (editorModel) {
|
||||
(window as any).monaco.editor.setModelLanguage(editorModel, newLang);
|
||||
}
|
||||
|
||||
// Update selected state
|
||||
langOptions.forEach(opt => opt.classList.remove('selected'));
|
||||
option.classList.add('selected');
|
||||
|
||||
updateToolbarUI(modal);
|
||||
updateFooterUI(modal);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Word wrap button
|
||||
const wrapBtn = toolbar.querySelector('.wrap-btn');
|
||||
wrapBtn?.addEventListener('click', async () => {
|
||||
modalWordWrap = modalWordWrap === 'on' ? 'off' : 'on';
|
||||
@@ -671,7 +838,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
updateToolbarUI(modal);
|
||||
});
|
||||
|
||||
// Line numbers button
|
||||
const linesBtn = toolbar.querySelector('.lines-btn');
|
||||
linesBtn?.addEventListener('click', async () => {
|
||||
modalShowLineNumbers = !modalShowLineNumbers;
|
||||
@@ -682,7 +848,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
updateToolbarUI(modal);
|
||||
});
|
||||
|
||||
// Copy button
|
||||
const copyBtn = toolbar.querySelector('.copy-btn');
|
||||
copyBtn?.addEventListener('click', async () => {
|
||||
if (modalEditorElement) {
|
||||
@@ -702,7 +867,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (modalLanguageDropdownOpen && !langBtn?.contains(e.target as Node)) {
|
||||
modalLanguageDropdownOpen = false;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
||||
import { demoFunc } from './demo.js';
|
||||
import { datepickerStyles } from './styles.js';
|
||||
import { renderDatepicker } from './template.js';
|
||||
import type { IDateEvent } from './types.js';
|
||||
import { DeesInputDatepickerPopup } from './datepicker-popup.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import '../../00group-layout/dees-label/dees-label.js';
|
||||
|
||||
@@ -49,7 +49,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
accessor disabledDates: string[] = [];
|
||||
|
||||
@property({ type: Number })
|
||||
accessor weekStartsOn: 0 | 1 = 1; // Default to Monday
|
||||
accessor weekStartsOn: 0 | 1 = 1;
|
||||
|
||||
@property({ type: String })
|
||||
accessor placeholder: string = 'YYYY-MM-DD';
|
||||
@@ -61,14 +61,11 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
accessor timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor events: IDateEvent[] = [];
|
||||
accessor events: import('./types.js').IDateEvent[] = [];
|
||||
|
||||
@state()
|
||||
accessor isOpened: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor opensToTop: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor selectedDate: Date | null = null;
|
||||
|
||||
@@ -81,57 +78,19 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
@state()
|
||||
accessor selectedMinute: number = 0;
|
||||
|
||||
private popupInstance: DeesInputDatepickerPopup | null = null;
|
||||
|
||||
public static styles = datepickerStyles;
|
||||
|
||||
|
||||
|
||||
public getTimezones(): { value: string; label: string }[] {
|
||||
// Common timezones with their display names
|
||||
return [
|
||||
{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
|
||||
{ value: 'America/New_York', label: 'Eastern Time (US & Canada)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (US & Canada)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (US & Canada)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' },
|
||||
{ value: 'America/Phoenix', label: 'Arizona' },
|
||||
{ value: 'America/Anchorage', label: 'Alaska' },
|
||||
{ value: 'Pacific/Honolulu', label: 'Hawaii' },
|
||||
{ value: 'Europe/London', label: 'London' },
|
||||
{ value: 'Europe/Paris', label: 'Paris' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin' },
|
||||
{ value: 'Europe/Moscow', label: 'Moscow' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai' },
|
||||
{ value: 'Asia/Kolkata', label: 'India Standard Time' },
|
||||
{ value: 'Asia/Shanghai', label: 'China Standard Time' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney' },
|
||||
{ value: 'Pacific/Auckland', label: 'Auckland' },
|
||||
];
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return renderDatepicker(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
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);
|
||||
@@ -160,19 +119,15 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
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();
|
||||
|
||||
// Replace in correct order to avoid conflicts
|
||||
|
||||
formatted = formatted.replace('YYYY', year);
|
||||
formatted = formatted.replace('YY', year.slice(-2));
|
||||
formatted = formatted.replace('MM', month);
|
||||
formatted = formatted.replace('DD', day);
|
||||
|
||||
// Time formatting if enabled
|
||||
if (this.enableTime) {
|
||||
const hours24 = date.getHours();
|
||||
const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24;
|
||||
@@ -186,17 +141,11 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
// Timezone formatting if enabled
|
||||
if (this.enableTimezone) {
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZoneName: 'short',
|
||||
timeZone: this.timezone
|
||||
});
|
||||
const formatter = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short', timeZone: this.timezone });
|
||||
const parts = formatter.formatToParts(date);
|
||||
const tzPart = parts.find(part => part.type === 'timeZoneName');
|
||||
if (tzPart) {
|
||||
formatted += ` ${tzPart.value}`;
|
||||
}
|
||||
if (tzPart) formatted += ` ${tzPart.value}`;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
@@ -205,274 +154,101 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
private handleClickOutside = (event: MouseEvent) => {
|
||||
const path = event.composedPath();
|
||||
if (!path.includes(this)) {
|
||||
this.isOpened = false;
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
};
|
||||
|
||||
public 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;
|
||||
this.closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add click outside listener
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
}, 0);
|
||||
} else {
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
this.isOpened = true;
|
||||
|
||||
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
||||
const rect = inputContainer.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
const opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
||||
|
||||
if (!this.popupInstance) {
|
||||
this.popupInstance = new DeesInputDatepickerPopup();
|
||||
}
|
||||
|
||||
// Configure popup
|
||||
this.popupInstance.triggerRect = rect;
|
||||
this.popupInstance.ownerComponent = this;
|
||||
this.popupInstance.opensToTop = opensToTop;
|
||||
this.popupInstance.enableTime = this.enableTime;
|
||||
this.popupInstance.timeFormat = this.timeFormat;
|
||||
this.popupInstance.minuteIncrement = this.minuteIncrement;
|
||||
this.popupInstance.weekStartsOn = this.weekStartsOn;
|
||||
this.popupInstance.minDate = this.minDate;
|
||||
this.popupInstance.maxDate = this.maxDate;
|
||||
this.popupInstance.disabledDates = this.disabledDates;
|
||||
this.popupInstance.enableTimezone = this.enableTimezone;
|
||||
this.popupInstance.timezone = this.timezone;
|
||||
this.popupInstance.events = this.events;
|
||||
this.popupInstance.selectedDate = this.selectedDate;
|
||||
this.popupInstance.viewDate = new Date(this.viewDate);
|
||||
this.popupInstance.selectedHour = this.selectedHour;
|
||||
this.popupInstance.selectedMinute = this.selectedMinute;
|
||||
|
||||
// Listen for popup events
|
||||
this.popupInstance.addEventListener('date-selected', this.handleDateSelected);
|
||||
this.popupInstance.addEventListener('date-cleared', this.handleDateCleared);
|
||||
this.popupInstance.addEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest);
|
||||
|
||||
await this.popupInstance.show();
|
||||
}
|
||||
|
||||
private closePopup(): void {
|
||||
this.isOpened = false;
|
||||
if (this.popupInstance) {
|
||||
this.popupInstance.removeEventListener('date-selected', this.handleDateSelected);
|
||||
this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared);
|
||||
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
|
||||
this.popupInstance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear();
|
||||
}
|
||||
|
||||
public 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();
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public getEventsForDate(date: Date): IDateEvent[] {
|
||||
if (!this.events || this.events.length === 0) return [];
|
||||
|
||||
const dateStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||
return this.events.filter(event => event.date === dateStr);
|
||||
}
|
||||
|
||||
public selectDate(date: Date): void {
|
||||
this.selectedDate = new Date(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate(),
|
||||
this.selectedHour,
|
||||
this.selectedMinute
|
||||
);
|
||||
|
||||
this.value = this.formatValueWithTimezone(this.selectedDate);
|
||||
private handleDateSelected = (event: Event): void => {
|
||||
const date = (event as CustomEvent).detail as Date;
|
||||
this.selectedDate = date;
|
||||
this.selectedHour = date.getHours();
|
||||
this.selectedMinute = date.getMinutes();
|
||||
this.viewDate = new Date(date);
|
||||
this.value = this.formatValueWithTimezone(date);
|
||||
this.changeSubject.next(this);
|
||||
|
||||
if (!this.enableTime) {
|
||||
this.isOpened = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public 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.formatValueWithTimezone(this.selectedDate);
|
||||
this.changeSubject.next(this);
|
||||
|
||||
if (!this.enableTime) {
|
||||
this.isOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
private handleDateCleared = (): void => {
|
||||
this.value = '';
|
||||
this.selectedDate = null;
|
||||
this.changeSubject.next(this);
|
||||
this.isOpened = false;
|
||||
}
|
||||
};
|
||||
|
||||
public previousMonth(): void {
|
||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
|
||||
}
|
||||
private handleCloseRequest = (): void => {
|
||||
this.closePopup();
|
||||
};
|
||||
|
||||
public nextMonth(): void {
|
||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
|
||||
}
|
||||
private handleRepositionRequest = (): void => {
|
||||
if (!this.popupInstance || !this.isOpened) return;
|
||||
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
||||
if (!inputContainer) return;
|
||||
|
||||
public 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));
|
||||
const rect = inputContainer.getBoundingClientRect();
|
||||
if (rect.bottom < 0 || rect.top > window.innerHeight) {
|
||||
this.closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSelectedDateTime();
|
||||
}
|
||||
|
||||
public 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();
|
||||
}
|
||||
|
||||
public 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.formatValueWithTimezone(this.selectedDate);
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
}
|
||||
|
||||
public handleTimezoneChange(e: Event): void {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this.timezone = select.value;
|
||||
this.updateSelectedDateTime();
|
||||
}
|
||||
|
||||
private formatValueWithTimezone(date: Date): string {
|
||||
if (!this.enableTimezone) {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
// Format the date with timezone offset
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: this.timezone,
|
||||
timeZoneName: 'short'
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const dateParts: any = {};
|
||||
parts.forEach(part => {
|
||||
dateParts[part.type] = part.value;
|
||||
});
|
||||
|
||||
// Create ISO-like format with timezone
|
||||
const isoString = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
|
||||
|
||||
// Get timezone offset
|
||||
const tzOffset = this.getTimezoneOffset(date, this.timezone);
|
||||
return `${isoString}${tzOffset}`;
|
||||
}
|
||||
|
||||
private getTimezoneOffset(date: Date, timezone: string): string {
|
||||
// Create a date in the target timezone
|
||||
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
|
||||
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
|
||||
const offsetMinutes = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60);
|
||||
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
|
||||
const minutes = Math.abs(offsetMinutes) % 60;
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||||
|
||||
return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
this.popupInstance.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
||||
this.popupInstance.triggerRect = rect;
|
||||
};
|
||||
|
||||
public handleKeydown(e: KeyboardEvent): void {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -480,7 +256,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
this.toggleCalendar();
|
||||
} else if (e.key === 'Escape' && this.isOpened) {
|
||||
e.preventDefault();
|
||||
this.isOpened = false;
|
||||
this.closePopup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,9 +270,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
public 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;
|
||||
@@ -504,7 +279,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
|
||||
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);
|
||||
@@ -517,7 +291,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
public handleInputBlur(e: FocusEvent): void {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const inputValue = input.value.trim();
|
||||
|
||||
|
||||
if (!inputValue) {
|
||||
this.value = '';
|
||||
this.selectedDate = null;
|
||||
@@ -533,10 +307,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -544,22 +316,17 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
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) {
|
||||
@@ -568,7 +335,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
// Format 3: MM/DD/YYYY (US)
|
||||
if (!parsedDate) {
|
||||
const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
||||
if (usMatch) {
|
||||
@@ -577,12 +343,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
// If no date was parsed, return null
|
||||
if (!parsedDate || isNaN(parsedDate.getTime())) {
|
||||
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) {
|
||||
@@ -591,7 +353,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
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());
|
||||
@@ -602,6 +363,34 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
return parsedDate;
|
||||
}
|
||||
|
||||
private formatValueWithTimezone(date: Date): string {
|
||||
if (!this.enableTimezone) return date.toISOString();
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: false, timeZone: this.timezone, timeZoneName: 'short',
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const dateParts: any = {};
|
||||
parts.forEach(part => { dateParts[part.type] = part.value; });
|
||||
|
||||
const isoString = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
|
||||
const tzOffset = this.getTimezoneOffset(date, this.timezone);
|
||||
return `${isoString}${tzOffset}`;
|
||||
}
|
||||
|
||||
private getTimezoneOffset(date: Date, timezone: string): string {
|
||||
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
|
||||
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
const offsetMinutes = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60);
|
||||
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
|
||||
const minutes = Math.abs(offsetMinutes) % 60;
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||||
return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
@@ -622,4 +411,16 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
if (this.popupInstance) {
|
||||
this.popupInstance.removeEventListener('date-selected', this.handleDateSelected);
|
||||
this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared);
|
||||
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
|
||||
this.popupInstance.hide();
|
||||
this.popupInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,758 @@
|
||||
import {
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
DeesElement,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { zIndexRegistry } from '../../00zindex.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import type { IDateEvent } from './types.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-input-datepicker-popup': DeesInputDatepickerPopup;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-input-datepicker-popup')
|
||||
export class DeesInputDatepickerPopup extends DeesElement {
|
||||
// Properties set by the parent
|
||||
@property({ attribute: false })
|
||||
accessor triggerRect: DOMRect | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor ownerComponent: HTMLElement | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableTime: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor timeFormat: '24h' | '12h' = '24h';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor minuteIncrement: number = 1;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor weekStartsOn: 0 | 1 = 1;
|
||||
|
||||
@property({ type: String })
|
||||
accessor minDate: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor maxDate: string = '';
|
||||
|
||||
@property({ type: Array })
|
||||
accessor disabledDates: string[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableTimezone: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor events: IDateEvent[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor opensToTop: boolean = false;
|
||||
|
||||
// Internal state
|
||||
@state()
|
||||
accessor selectedDate: Date | null = null;
|
||||
|
||||
@state()
|
||||
accessor viewDate: Date = new Date();
|
||||
|
||||
@state()
|
||||
accessor selectedHour: number = 0;
|
||||
|
||||
@state()
|
||||
accessor selectedMinute: number = 0;
|
||||
|
||||
@state()
|
||||
accessor menuZIndex: number = 1000;
|
||||
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
private windowLayer: DeesWindowLayer | null = null;
|
||||
private isDestroying: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.calendar-popup {
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
will-change: transform, opacity;
|
||||
transition: all 0.15s 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;
|
||||
user-select: none;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.calendar-popup.top {
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.calendar-popup.show {
|
||||
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%)')};
|
||||
}
|
||||
|
||||
/* Weekday headers */
|
||||
.weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
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;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Event indicators */
|
||||
.event-indicator {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.event-dot.info { background: ${cssManager.bdTheme('hsl(211 70% 52%)', 'hsl(211 70% 62%)')}; }
|
||||
.event-dot.warning { background: ${cssManager.bdTheme('hsl(45 90% 45%)', 'hsl(45 90% 55%)')}; }
|
||||
.event-dot.success { background: ${cssManager.bdTheme('hsl(142 69% 45%)', 'hsl(142 69% 55%)')}; }
|
||||
.event-dot.error { background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')}; }
|
||||
|
||||
.event-count {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.event-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.day.has-event:hover .event-tooltip { opacity: 1; }
|
||||
|
||||
/* 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: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%)')};
|
||||
}
|
||||
|
||||
/* Timezone selector */
|
||||
.timezone-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%)')};
|
||||
}
|
||||
|
||||
.timezone-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%)')};
|
||||
}
|
||||
|
||||
.timezone-select {
|
||||
width: 100%;
|
||||
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;
|
||||
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%)')};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 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%)')};
|
||||
}
|
||||
|
||||
.clear-action-button {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.clear-action-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%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private static readonly MONTH_NAMES = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
];
|
||||
|
||||
private static readonly TIMEZONES = [
|
||||
{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
|
||||
{ value: 'America/New_York', label: 'Eastern Time (US & Canada)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (US & Canada)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (US & Canada)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' },
|
||||
{ value: 'Europe/London', label: 'London' },
|
||||
{ value: 'Europe/Paris', label: 'Paris' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin' },
|
||||
{ value: 'Europe/Moscow', label: 'Moscow' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai' },
|
||||
{ value: 'Asia/Kolkata', label: 'India Standard Time' },
|
||||
{ value: 'Asia/Shanghai', label: 'China Standard Time' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney' },
|
||||
{ value: 'Pacific/Auckland', label: 'Auckland' },
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.triggerRect) return html``;
|
||||
|
||||
const posStyle = this.computePositionStyle();
|
||||
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="calendar-popup ${this.visible ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}"
|
||||
style="${posStyle}; z-index: ${this.menuZIndex};"
|
||||
>
|
||||
<div class="calendar-header">
|
||||
<button class="nav-button" @click=${this.previousMonth}>
|
||||
<dees-icon icon="lucide:chevronLeft" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
<div class="month-year-display">
|
||||
${DeesInputDatepickerPopup.MONTH_NAMES[this.viewDate.getMonth()]} ${this.viewDate.getFullYear()}
|
||||
</div>
|
||||
<button class="nav-button" @click=${this.nextMonth}>
|
||||
<dees-icon icon="lucide:chevronRight" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="weekdays">
|
||||
${weekDays.map(day => html`<div class="weekday">${day}</div>`)}
|
||||
</div>
|
||||
|
||||
<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 isDayDisabled = this.isDayDisabled(day);
|
||||
const dayEvents = this.getEventsForDate(day);
|
||||
const hasEvents = dayEvents.length > 0;
|
||||
const totalEventCount = dayEvents.reduce((sum, event) => sum + (event.count || 1), 0);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${isDayDisabled ? 'disabled' : ''} ${hasEvents ? 'has-event' : ''}"
|
||||
@click=${() => !isDayDisabled && this.handleSelectDate(day)}
|
||||
>
|
||||
${day.getDate()}
|
||||
${hasEvents ? html`
|
||||
${totalEventCount > 3 ? html`
|
||||
<div class="event-count">${totalEventCount}</div>
|
||||
` : html`
|
||||
<div class="event-indicator">
|
||||
${dayEvents.slice(0, 3).map(event => html`
|
||||
<div class="event-dot ${event.type || 'info'}"></div>
|
||||
`)}
|
||||
</div>
|
||||
`}
|
||||
${dayEvents[0].title ? html`
|
||||
<div class="event-tooltip">
|
||||
${dayEvents[0].title}
|
||||
${totalEventCount > 1 ? html` (+${totalEventCount - 1} more)` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
${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=${this.handleHourInput}
|
||||
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=${this.handleMinuteInput}
|
||||
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>
|
||||
` : ''}
|
||||
|
||||
${this.enableTimezone ? html`
|
||||
<div class="timezone-selector">
|
||||
<div class="timezone-selector-title">Timezone</div>
|
||||
<select class="timezone-select" .value=${this.timezone} @change=${this.handleTimezoneChange}>
|
||||
${DeesInputDatepickerPopup.TIMEZONES.map(tz => html`
|
||||
<option value="${tz.value}" ?selected=${tz.value === this.timezone}>${tz.label}</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="calendar-actions">
|
||||
<button class="action-button today-button" @click=${this.handleSelectToday}>Today</button>
|
||||
<button class="action-button clear-action-button" @click=${this.handleClear}>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private computePositionStyle(): string {
|
||||
const rect = this.triggerRect!;
|
||||
const left = rect.left;
|
||||
|
||||
if (this.opensToTop) {
|
||||
const bottom = window.innerHeight - rect.top + 4;
|
||||
return `left: ${left}px; bottom: ${bottom}px; top: auto`;
|
||||
} else {
|
||||
const top = rect.bottom + 4;
|
||||
return `left: ${left}px; top: ${top}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar logic
|
||||
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[] = [];
|
||||
|
||||
const startOffset = this.weekStartsOn === 1
|
||||
? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
|
||||
: firstDay.getDay();
|
||||
|
||||
for (let i = startOffset; i > 0; i--) days.push(new Date(year, month, 1 - i));
|
||||
for (let i = 1; i <= lastDay.getDate(); i++) days.push(new Date(year, month, i));
|
||||
const remaining = 42 - days.length;
|
||||
for (let i = 1; i <= remaining; 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 isDayDisabled(date: Date): boolean {
|
||||
if (this.minDate) { const min = new Date(this.minDate); if (date < min) return true; }
|
||||
if (this.maxDate) { const max = new Date(this.maxDate); if (date > max) return true; }
|
||||
if (this.disabledDates?.length) {
|
||||
return this.disabledDates.some(ds => {
|
||||
try { const d = new Date(ds); return date.getDate() === d.getDate() && date.getMonth() === d.getMonth() && date.getFullYear() === d.getFullYear(); }
|
||||
catch { return false; }
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private getEventsForDate(date: Date): IDateEvent[] {
|
||||
if (!this.events?.length) return [];
|
||||
const dateStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||
return this.events.filter(e => e.date === dateStr);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Event dispatching
|
||||
private handleSelectDate(day: Date): void {
|
||||
this.selectedDate = new Date(day.getFullYear(), day.getMonth(), day.getDate(), this.selectedHour, this.selectedMinute);
|
||||
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
||||
if (!this.enableTime) {
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelectToday(): void {
|
||||
const today = new Date();
|
||||
this.selectedDate = today;
|
||||
this.viewDate = new Date(today);
|
||||
this.selectedHour = today.getHours();
|
||||
this.selectedMinute = today.getMinutes();
|
||||
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
||||
if (!this.enableTime) {
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
}
|
||||
}
|
||||
|
||||
private handleClear(): void {
|
||||
this.dispatchEvent(new CustomEvent('date-cleared'));
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
}
|
||||
|
||||
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));
|
||||
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.emitTimeUpdate();
|
||||
};
|
||||
|
||||
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 > 1) value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
|
||||
this.selectedMinute = value;
|
||||
this.emitTimeUpdate();
|
||||
};
|
||||
|
||||
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.emitTimeUpdate();
|
||||
}
|
||||
|
||||
private handleTimezoneChange = (e: Event): void => {
|
||||
this.timezone = (e.target as HTMLSelectElement).value;
|
||||
this.emitTimeUpdate();
|
||||
};
|
||||
|
||||
private emitTimeUpdate(): void {
|
||||
if (this.selectedDate) {
|
||||
this.selectedDate = new Date(this.selectedDate.getFullYear(), this.selectedDate.getMonth(), this.selectedDate.getDate(), this.selectedHour, this.selectedMinute);
|
||||
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide lifecycle
|
||||
public async show(): Promise<void> {
|
||||
this.windowLayer = await DeesWindowLayer.createAndShow();
|
||||
this.windowLayer.addEventListener('click', () => {
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
});
|
||||
|
||||
this.menuZIndex = zIndexRegistry.getNextZIndex();
|
||||
zIndexRegistry.register(this, this.menuZIndex);
|
||||
this.style.zIndex = this.menuZIndex.toString();
|
||||
|
||||
document.body.appendChild(this);
|
||||
|
||||
await domtools.plugins.smartdelay.delayFor(0);
|
||||
this.visible = true;
|
||||
|
||||
window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true });
|
||||
window.addEventListener('resize', this.handleScrollOrResize, { passive: true });
|
||||
}
|
||||
|
||||
public async hide(): Promise<void> {
|
||||
if (this.isDestroying) return;
|
||||
this.isDestroying = true;
|
||||
|
||||
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
||||
window.removeEventListener('resize', this.handleScrollOrResize);
|
||||
zIndexRegistry.unregister(this);
|
||||
|
||||
if (this.windowLayer) {
|
||||
this.windowLayer.destroy();
|
||||
this.windowLayer = null;
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
await domtools.plugins.smartdelay.delayFor(150);
|
||||
|
||||
if (this.parentElement) this.parentElement.removeChild(this);
|
||||
this.isDestroying = false;
|
||||
}
|
||||
|
||||
private handleScrollOrResize = (): void => {
|
||||
this.dispatchEvent(new CustomEvent('reposition-request'));
|
||||
};
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
||||
window.removeEventListener('resize', this.handleScrollOrResize);
|
||||
zIndexRegistry.unregister(this);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './component.js';
|
||||
export * from './datepicker-popup.js';
|
||||
|
||||
@@ -56,7 +56,6 @@ export const datepickerStyles = [
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Icon container using flexbox for better positioning */
|
||||
.icon-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -101,414 +100,5 @@ export const datepickerStyles = [
|
||||
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;
|
||||
}
|
||||
|
||||
/* Event indicators */
|
||||
.day.has-event {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-indicator {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.event-dot.info {
|
||||
background: ${cssManager.bdTheme('hsl(211 70% 52%)', 'hsl(211 70% 62%)')};
|
||||
}
|
||||
|
||||
.event-dot.warning {
|
||||
background: ${cssManager.bdTheme('hsl(45 90% 45%)', 'hsl(45 90% 55%)')};
|
||||
}
|
||||
|
||||
.event-dot.success {
|
||||
background: ${cssManager.bdTheme('hsl(142 69% 45%)', 'hsl(142 69% 55%)')};
|
||||
}
|
||||
|
||||
.event-dot.error {
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')};
|
||||
}
|
||||
|
||||
.event-count {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Tooltip for event details */
|
||||
.event-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.event-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.day.has-event:hover .event-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 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)')};
|
||||
}
|
||||
|
||||
/* Timezone selector */
|
||||
.timezone-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%)')};
|
||||
}
|
||||
|
||||
.timezone-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%)')};
|
||||
}
|
||||
|
||||
.timezone-select {
|
||||
width: 100%;
|
||||
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;
|
||||
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%)')};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.timezone-select: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%)')};
|
||||
}
|
||||
|
||||
.timezone-select: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)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
];
|
||||
|
||||
@@ -2,19 +2,6 @@ import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||
import type { DeesInputDatepicker } from './component.js';
|
||||
|
||||
export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult => {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
|
||||
const weekDays = component.weekStartsOn === 1
|
||||
? ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
||||
: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
|
||||
const days = component.getDaysInMonth();
|
||||
const isAM = component.selectedHour < 12;
|
||||
const timezones = component.getTimezones();
|
||||
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${component.label} .description=${component.description} .required=${component.required}></dees-label>
|
||||
@@ -39,141 +26,8 @@ export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult
|
||||
` : ''}
|
||||
<dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Popup -->
|
||||
<div class="calendar-popup ${component.isOpened ? 'show' : ''} ${component.opensToTop ? 'top' : 'bottom'}">
|
||||
<!-- Month/Year Navigation -->
|
||||
<div class="calendar-header">
|
||||
<button class="nav-button" @click=${component.previousMonth}>
|
||||
<dees-icon icon="lucide:chevronLeft" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
<div class="month-year-display">
|
||||
${monthNames[component.viewDate.getMonth()]} ${component.viewDate.getFullYear()}
|
||||
</div>
|
||||
<button class="nav-button" @click=${component.nextMonth}>
|
||||
<dees-icon icon="lucide:chevronRight" iconSize="16"></dees-icon>
|
||||
</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 = component.isToday(day);
|
||||
const isSelected = component.isSelected(day);
|
||||
const isOtherMonth = day.getMonth() !== component.viewDate.getMonth();
|
||||
const isDisabled = component.isDisabled(day);
|
||||
const dayEvents = component.getEventsForDate(day);
|
||||
const hasEvents = dayEvents.length > 0;
|
||||
const totalEventCount = dayEvents.reduce((sum, event) => sum + (event.count || 1), 0);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''} ${hasEvents ? 'has-event' : ''}"
|
||||
@click=${() => !isDisabled && component.selectDate(day)}
|
||||
>
|
||||
${day.getDate()}
|
||||
${hasEvents ? html`
|
||||
${totalEventCount > 3 ? html`
|
||||
<div class="event-count">${totalEventCount}</div>
|
||||
` : html`
|
||||
<div class="event-indicator">
|
||||
${dayEvents.slice(0, 3).map(event => html`
|
||||
<div class="event-dot ${event.type || 'info'}"></div>
|
||||
`)}
|
||||
</div>
|
||||
`}
|
||||
${dayEvents[0].title ? html`
|
||||
<div class="event-tooltip">
|
||||
${dayEvents[0].title}
|
||||
${totalEventCount > 1 ? html` (+${totalEventCount - 1} more)` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Time Selector -->
|
||||
${component.enableTime ? html`
|
||||
<div class="time-selector">
|
||||
<div class="time-selector-title">Time</div>
|
||||
<div class="time-inputs">
|
||||
<input
|
||||
type="number"
|
||||
class="time-input"
|
||||
.value=${component.timeFormat === '12h'
|
||||
? (component.selectedHour === 0 ? 12 : component.selectedHour > 12 ? component.selectedHour - 12 : component.selectedHour).toString().padStart(2, '0')
|
||||
: component.selectedHour.toString().padStart(2, '0')}
|
||||
@input=${(e: InputEvent) => component.handleHourInput(e)}
|
||||
min="${component.timeFormat === '12h' ? 1 : 0}"
|
||||
max="${component.timeFormat === '12h' ? 12 : 23}"
|
||||
/>
|
||||
<span class="time-separator">:</span>
|
||||
<input
|
||||
type="number"
|
||||
class="time-input"
|
||||
.value=${component.selectedMinute.toString().padStart(2, '0')}
|
||||
@input=${(e: InputEvent) => component.handleMinuteInput(e)}
|
||||
min="0"
|
||||
max="59"
|
||||
step="${component.minuteIncrement || 1}"
|
||||
/>
|
||||
${component.timeFormat === '12h' ? html`
|
||||
<div class="am-pm-selector">
|
||||
<button
|
||||
class="am-pm-button ${isAM ? 'selected' : ''}"
|
||||
@click=${() => component.setAMPM('am')}
|
||||
>
|
||||
AM
|
||||
</button>
|
||||
<button
|
||||
class="am-pm-button ${!isAM ? 'selected' : ''}"
|
||||
@click=${() => component.setAMPM('pm')}
|
||||
>
|
||||
PM
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Timezone Selector -->
|
||||
${component.enableTimezone ? html`
|
||||
<div class="timezone-selector">
|
||||
<div class="timezone-selector-title">Timezone</div>
|
||||
<select
|
||||
class="timezone-select"
|
||||
.value=${component.timezone}
|
||||
@change=${(e: Event) => component.handleTimezoneChange(e)}
|
||||
>
|
||||
${timezones.map(tz => html`
|
||||
<option value="${tz.value}" ?selected=${tz.value === component.timezone}>
|
||||
${tz.label}
|
||||
</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="calendar-actions">
|
||||
<button class="action-button today-button" @click=${component.selectToday}>
|
||||
Today
|
||||
</button>
|
||||
<button class="action-button clear-button" @click=${component.clear}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user