feat(input): extract datepicker popup into a window-layer overlay and enhance the code editor modal status UI
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-04-05 - 3.58.0 - feat(dees-input-code)
|
||||||
add editor status footer with cursor position, line count, and language display
|
add editor status footer with cursor position, line count, and language display
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.58.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.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -508,23 +508,15 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
||||||
if (!toolbar) return;
|
if (!toolbar) return;
|
||||||
|
|
||||||
// Update language button text
|
|
||||||
const langBtn = toolbar.querySelector('.language-button span');
|
const langBtn = toolbar.querySelector('.language-button span');
|
||||||
if (langBtn) langBtn.textContent = getLanguageLabel();
|
if (langBtn) langBtn.textContent = getLanguageLabel();
|
||||||
|
|
||||||
// Update word wrap button
|
|
||||||
const wrapBtn = toolbar.querySelector('.wrap-btn') as HTMLElement;
|
const wrapBtn = toolbar.querySelector('.wrap-btn') as HTMLElement;
|
||||||
if (wrapBtn) {
|
if (wrapBtn) wrapBtn.classList.toggle('active', modalWordWrap === 'on');
|
||||||
wrapBtn.classList.toggle('active', modalWordWrap === 'on');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update line numbers button
|
|
||||||
const linesBtn = toolbar.querySelector('.lines-btn') as HTMLElement;
|
const linesBtn = toolbar.querySelector('.lines-btn') as HTMLElement;
|
||||||
if (linesBtn) {
|
if (linesBtn) linesBtn.classList.toggle('active', modalShowLineNumbers);
|
||||||
linesBtn.classList.toggle('active', modalShowLineNumbers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update copy button
|
|
||||||
const copyBtn = toolbar.querySelector('.copy-btn') as HTMLElement;
|
const copyBtn = toolbar.querySelector('.copy-btn') as HTMLElement;
|
||||||
const copyIcon = copyBtn?.querySelector('dees-icon') as any;
|
const copyIcon = copyBtn?.querySelector('dees-icon') as any;
|
||||||
if (copyBtn && copyIcon) {
|
if (copyBtn && copyIcon) {
|
||||||
@@ -532,13 +524,28 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
copyIcon.icon = modalCopySuccess ? 'lucide:Check' : 'lucide:Copy';
|
copyIcon.icon = modalCopySuccess ? 'lucide:Check' : 'lucide:Copy';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update dropdown visibility
|
|
||||||
const dropdown = toolbar.querySelector('.language-dropdown') as HTMLElement;
|
const dropdown = toolbar.querySelector('.language-dropdown') as HTMLElement;
|
||||||
if (dropdown) {
|
if (dropdown) dropdown.style.display = modalLanguageDropdownOpen ? 'block' : 'none';
|
||||||
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({
|
const modal = await DeesModal.createAndShow({
|
||||||
heading: this.label || 'Code Editor',
|
heading: this.label || 'Code Editor',
|
||||||
width: 'fullscreen',
|
width: 'fullscreen',
|
||||||
@@ -549,9 +556,7 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 12px;
|
padding: 4px 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%)')};
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.modal-toolbar .toolbar-left {
|
.modal-toolbar .toolbar-left {
|
||||||
@@ -644,9 +649,30 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
.modal-editor-wrapper {
|
.modal-editor-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: calc(100vh - 175px);
|
height: calc(100vh - 200px);
|
||||||
width: 100%;
|
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>
|
</style>
|
||||||
<div class="modal-toolbar">
|
<div class="modal-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
@@ -687,6 +713,16 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
.wordWrap=${modalWordWrap}
|
.wordWrap=${modalWordWrap}
|
||||||
></dees-workspace-monaco>
|
></dees-workspace-monaco>
|
||||||
</div>
|
</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: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
@@ -698,7 +734,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
{
|
{
|
||||||
name: 'Save & Close',
|
name: 'Save & Close',
|
||||||
action: async (modalRef) => {
|
action: async (modalRef) => {
|
||||||
// Get the editor content from the modal
|
|
||||||
modalEditorElement = modalRef!.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
modalEditorElement = modalRef!.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
||||||
if (modalEditorElement) {
|
if (modalEditorElement) {
|
||||||
const editor = await modalEditorElement.editorDeferred.promise;
|
const editor = await modalEditorElement.editorDeferred.promise;
|
||||||
@@ -715,17 +750,61 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
modalEditorElement = modal.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
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
|
// Wire up toolbar event handlers
|
||||||
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
||||||
if (toolbar) {
|
if (toolbar) {
|
||||||
// Language button click
|
|
||||||
const langBtn = toolbar.querySelector('.language-button');
|
const langBtn = toolbar.querySelector('.language-button');
|
||||||
langBtn?.addEventListener('click', () => {
|
langBtn?.addEventListener('click', () => {
|
||||||
modalLanguageDropdownOpen = !modalLanguageDropdownOpen;
|
modalLanguageDropdownOpen = !modalLanguageDropdownOpen;
|
||||||
updateToolbarUI(modal);
|
updateToolbarUI(modal);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Language option clicks
|
|
||||||
const langOptions = toolbar.querySelectorAll('.language-option');
|
const langOptions = toolbar.querySelectorAll('.language-option');
|
||||||
langOptions.forEach((option) => {
|
langOptions.forEach((option) => {
|
||||||
option.addEventListener('click', async () => {
|
option.addEventListener('click', async () => {
|
||||||
@@ -734,23 +813,21 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
modalLanguage = newLang;
|
modalLanguage = newLang;
|
||||||
modalLanguageDropdownOpen = false;
|
modalLanguageDropdownOpen = false;
|
||||||
|
|
||||||
// Update editor language
|
|
||||||
const editor = await modalEditorElement.editorDeferred.promise;
|
const editor = await modalEditorElement.editorDeferred.promise;
|
||||||
const model = editor.getModel();
|
const editorModel = editor.getModel();
|
||||||
if (model) {
|
if (editorModel) {
|
||||||
(window as any).monaco.editor.setModelLanguage(model, newLang);
|
(window as any).monaco.editor.setModelLanguage(editorModel, newLang);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update selected state
|
|
||||||
langOptions.forEach(opt => opt.classList.remove('selected'));
|
langOptions.forEach(opt => opt.classList.remove('selected'));
|
||||||
option.classList.add('selected');
|
option.classList.add('selected');
|
||||||
|
|
||||||
updateToolbarUI(modal);
|
updateToolbarUI(modal);
|
||||||
|
updateFooterUI(modal);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Word wrap button
|
|
||||||
const wrapBtn = toolbar.querySelector('.wrap-btn');
|
const wrapBtn = toolbar.querySelector('.wrap-btn');
|
||||||
wrapBtn?.addEventListener('click', async () => {
|
wrapBtn?.addEventListener('click', async () => {
|
||||||
modalWordWrap = modalWordWrap === 'on' ? 'off' : 'on';
|
modalWordWrap = modalWordWrap === 'on' ? 'off' : 'on';
|
||||||
@@ -761,7 +838,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
updateToolbarUI(modal);
|
updateToolbarUI(modal);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Line numbers button
|
|
||||||
const linesBtn = toolbar.querySelector('.lines-btn');
|
const linesBtn = toolbar.querySelector('.lines-btn');
|
||||||
linesBtn?.addEventListener('click', async () => {
|
linesBtn?.addEventListener('click', async () => {
|
||||||
modalShowLineNumbers = !modalShowLineNumbers;
|
modalShowLineNumbers = !modalShowLineNumbers;
|
||||||
@@ -772,7 +848,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
updateToolbarUI(modal);
|
updateToolbarUI(modal);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy button
|
|
||||||
const copyBtn = toolbar.querySelector('.copy-btn');
|
const copyBtn = toolbar.querySelector('.copy-btn');
|
||||||
copyBtn?.addEventListener('click', async () => {
|
copyBtn?.addEventListener('click', async () => {
|
||||||
if (modalEditorElement) {
|
if (modalEditorElement) {
|
||||||
@@ -792,7 +867,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (modalLanguageDropdownOpen && !langBtn?.contains(e.target as Node)) {
|
if (modalLanguageDropdownOpen && !langBtn?.contains(e.target as Node)) {
|
||||||
modalLanguageDropdownOpen = false;
|
modalLanguageDropdownOpen = false;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
|||||||
import { demoFunc } from './demo.js';
|
import { demoFunc } from './demo.js';
|
||||||
import { datepickerStyles } from './styles.js';
|
import { datepickerStyles } from './styles.js';
|
||||||
import { renderDatepicker } from './template.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-utility/dees-icon/dees-icon.js';
|
||||||
import '../../00group-layout/dees-label/dees-label.js';
|
import '../../00group-layout/dees-label/dees-label.js';
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
accessor disabledDates: string[] = [];
|
accessor disabledDates: string[] = [];
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
accessor weekStartsOn: 0 | 1 = 1; // Default to Monday
|
accessor weekStartsOn: 0 | 1 = 1;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
accessor placeholder: string = 'YYYY-MM-DD';
|
accessor placeholder: string = 'YYYY-MM-DD';
|
||||||
@@ -61,14 +61,11 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
accessor timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
accessor timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
accessor events: IDateEvent[] = [];
|
accessor events: import('./types.js').IDateEvent[] = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor isOpened: boolean = false;
|
accessor isOpened: boolean = false;
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor opensToTop: boolean = false;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor selectedDate: Date | null = null;
|
accessor selectedDate: Date | null = null;
|
||||||
|
|
||||||
@@ -81,57 +78,19 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
@state()
|
@state()
|
||||||
accessor selectedMinute: number = 0;
|
accessor selectedMinute: number = 0;
|
||||||
|
|
||||||
|
private popupInstance: DeesInputDatepickerPopup | null = null;
|
||||||
|
|
||||||
public static styles = datepickerStyles;
|
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 {
|
public render(): TemplateResult {
|
||||||
return renderDatepicker(this);
|
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() {
|
async firstUpdated() {
|
||||||
// Initialize with empty value if not set
|
|
||||||
if (!this.value) {
|
if (!this.value) {
|
||||||
this.value = '';
|
this.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize view date and selected time
|
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
try {
|
try {
|
||||||
const date = new Date(this.value);
|
const date = new Date(this.value);
|
||||||
@@ -160,19 +119,15 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
if (isNaN(date.getTime())) return '';
|
if (isNaN(date.getTime())) return '';
|
||||||
|
|
||||||
let formatted = this.dateFormat;
|
let formatted = this.dateFormat;
|
||||||
|
|
||||||
// Basic date formatting
|
|
||||||
const day = date.getDate().toString().padStart(2, '0');
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
const year = date.getFullYear().toString();
|
const year = date.getFullYear().toString();
|
||||||
|
|
||||||
// Replace in correct order to avoid conflicts
|
|
||||||
formatted = formatted.replace('YYYY', year);
|
formatted = formatted.replace('YYYY', year);
|
||||||
formatted = formatted.replace('YY', year.slice(-2));
|
formatted = formatted.replace('YY', year.slice(-2));
|
||||||
formatted = formatted.replace('MM', month);
|
formatted = formatted.replace('MM', month);
|
||||||
formatted = formatted.replace('DD', day);
|
formatted = formatted.replace('DD', day);
|
||||||
|
|
||||||
// Time formatting if enabled
|
|
||||||
if (this.enableTime) {
|
if (this.enableTime) {
|
||||||
const hours24 = date.getHours();
|
const hours24 = date.getHours();
|
||||||
const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24;
|
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) {
|
if (this.enableTimezone) {
|
||||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
const formatter = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short', timeZone: this.timezone });
|
||||||
timeZoneName: 'short',
|
|
||||||
timeZone: this.timezone
|
|
||||||
});
|
|
||||||
const parts = formatter.formatToParts(date);
|
const parts = formatter.formatToParts(date);
|
||||||
const tzPart = parts.find(part => part.type === 'timeZoneName');
|
const tzPart = parts.find(part => part.type === 'timeZoneName');
|
||||||
if (tzPart) {
|
if (tzPart) formatted += ` ${tzPart.value}`;
|
||||||
formatted += ` ${tzPart.value}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatted;
|
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> {
|
public async toggleCalendar(): Promise<void> {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
this.isOpened = !this.isOpened;
|
|
||||||
|
|
||||||
if (this.isOpened) {
|
if (this.isOpened) {
|
||||||
// Check available space and set position
|
this.closePopup();
|
||||||
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
return;
|
||||||
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
|
this.isOpened = true;
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', this.handleClickOutside);
|
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
||||||
}, 0);
|
const rect = inputContainer.getBoundingClientRect();
|
||||||
} else {
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
document.removeEventListener('click', this.handleClickOutside);
|
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[] {
|
private handleDateSelected = (event: Event): void => {
|
||||||
const year = this.viewDate.getFullYear();
|
const date = (event as CustomEvent).detail as Date;
|
||||||
const month = this.viewDate.getMonth();
|
this.selectedDate = date;
|
||||||
const firstDay = new Date(year, month, 1);
|
this.selectedHour = date.getHours();
|
||||||
const lastDay = new Date(year, month + 1, 0);
|
this.selectedMinute = date.getMinutes();
|
||||||
const days: Date[] = [];
|
this.viewDate = new Date(date);
|
||||||
|
this.value = this.formatValueWithTimezone(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);
|
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
|
};
|
||||||
if (!this.enableTime) {
|
|
||||||
this.isOpened = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectToday(): void {
|
private handleDateCleared = (): 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 {
|
|
||||||
this.value = '';
|
this.value = '';
|
||||||
this.selectedDate = null;
|
this.selectedDate = null;
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
this.isOpened = false;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
public previousMonth(): void {
|
private handleCloseRequest = (): void => {
|
||||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
|
this.closePopup();
|
||||||
}
|
};
|
||||||
|
|
||||||
public nextMonth(): void {
|
private handleRepositionRequest = (): void => {
|
||||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
|
if (!this.popupInstance || !this.isOpened) return;
|
||||||
}
|
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
||||||
|
if (!inputContainer) return;
|
||||||
|
|
||||||
public handleHourInput(e: InputEvent): void {
|
const rect = inputContainer.getBoundingClientRect();
|
||||||
const input = e.target as HTMLInputElement;
|
if (rect.bottom < 0 || rect.top > window.innerHeight) {
|
||||||
let value = parseInt(input.value) || 0;
|
this.closePopup();
|
||||||
|
return;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMinuteInput(e: InputEvent): void {
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
const input = e.target as HTMLInputElement;
|
const spaceAbove = rect.top;
|
||||||
let value = parseInt(input.value) || 0;
|
this.popupInstance.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
||||||
value = Math.max(0, Math.min(59, value));
|
this.popupInstance.triggerRect = rect;
|
||||||
|
};
|
||||||
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')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleKeydown(e: KeyboardEvent): void {
|
public handleKeydown(e: KeyboardEvent): void {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
@@ -480,7 +256,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
this.toggleCalendar();
|
this.toggleCalendar();
|
||||||
} else if (e.key === 'Escape' && this.isOpened) {
|
} else if (e.key === 'Escape' && this.isOpened) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.isOpened = false;
|
this.closePopup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,9 +270,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
public handleManualInput(e: InputEvent): void {
|
public handleManualInput(e: InputEvent): void {
|
||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
const inputValue = input.value.trim();
|
const inputValue = input.value.trim();
|
||||||
|
|
||||||
if (!inputValue) {
|
if (!inputValue) {
|
||||||
// Clear the value if input is empty
|
|
||||||
this.value = '';
|
this.value = '';
|
||||||
this.selectedDate = null;
|
this.selectedDate = null;
|
||||||
return;
|
return;
|
||||||
@@ -504,7 +279,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
|
|
||||||
const parsedDate = this.parseManualDate(inputValue);
|
const parsedDate = this.parseManualDate(inputValue);
|
||||||
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
||||||
// Update internal state without triggering re-render of input
|
|
||||||
this.value = parsedDate.toISOString();
|
this.value = parsedDate.toISOString();
|
||||||
this.selectedDate = parsedDate;
|
this.selectedDate = parsedDate;
|
||||||
this.viewDate = new Date(parsedDate);
|
this.viewDate = new Date(parsedDate);
|
||||||
@@ -517,7 +291,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
public handleInputBlur(e: FocusEvent): void {
|
public handleInputBlur(e: FocusEvent): void {
|
||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
const inputValue = input.value.trim();
|
const inputValue = input.value.trim();
|
||||||
|
|
||||||
if (!inputValue) {
|
if (!inputValue) {
|
||||||
this.value = '';
|
this.value = '';
|
||||||
this.selectedDate = null;
|
this.selectedDate = null;
|
||||||
@@ -533,10 +307,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
this.selectedHour = parsedDate.getHours();
|
this.selectedHour = parsedDate.getHours();
|
||||||
this.selectedMinute = parsedDate.getMinutes();
|
this.selectedMinute = parsedDate.getMinutes();
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
// Update the input with formatted date
|
|
||||||
input.value = this.formatDate(this.value);
|
input.value = this.formatDate(this.value);
|
||||||
} else {
|
} else {
|
||||||
// Revert to previous valid value on blur if parsing failed
|
|
||||||
input.value = this.formatDate(this.value);
|
input.value = this.formatDate(this.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,22 +316,17 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
private parseManualDate(input: string): Date | null {
|
private parseManualDate(input: string): Date | null {
|
||||||
if (!input) return null;
|
if (!input) return null;
|
||||||
|
|
||||||
// Split date and time parts if present
|
|
||||||
const parts = input.split(' ');
|
const parts = input.split(' ');
|
||||||
let datePart = parts[0];
|
let datePart = parts[0];
|
||||||
let timePart = parts[1] || '';
|
let timePart = parts[1] || '';
|
||||||
|
|
||||||
let parsedDate: Date | null = null;
|
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})$/);
|
const isoMatch = datePart.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
||||||
if (isoMatch) {
|
if (isoMatch) {
|
||||||
const [_, year, month, day] = isoMatch;
|
const [_, year, month, day] = isoMatch;
|
||||||
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format 2: DD.MM.YYYY (European)
|
|
||||||
if (!parsedDate) {
|
if (!parsedDate) {
|
||||||
const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||||
if (euMatch) {
|
if (euMatch) {
|
||||||
@@ -568,7 +335,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format 3: MM/DD/YYYY (US)
|
|
||||||
if (!parsedDate) {
|
if (!parsedDate) {
|
||||||
const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
||||||
if (usMatch) {
|
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) {
|
if (timePart) {
|
||||||
const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/);
|
const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/);
|
||||||
if (timeMatch) {
|
if (timeMatch) {
|
||||||
@@ -591,7 +353,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
parsedDate.setMinutes(parseInt(minutes));
|
parsedDate.setMinutes(parseInt(minutes));
|
||||||
}
|
}
|
||||||
} else if (!this.enableTime) {
|
} else if (!this.enableTime) {
|
||||||
// If time is not enabled and not provided, use current time
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
parsedDate.setHours(now.getHours());
|
parsedDate.setHours(now.getHours());
|
||||||
parsedDate.setMinutes(now.getMinutes());
|
parsedDate.setMinutes(now.getMinutes());
|
||||||
@@ -602,6 +363,34 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|||||||
return parsedDate;
|
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 {
|
public getValue(): string {
|
||||||
return this.value;
|
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 './component.js';
|
||||||
|
export * from './datepicker-popup.js';
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export const datepickerStyles = [
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon container using flexbox for better positioning */
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -101,414 +100,5 @@ export const datepickerStyles = [
|
|||||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
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%)')};
|
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';
|
import type { DeesInputDatepicker } from './component.js';
|
||||||
|
|
||||||
export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult => {
|
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`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${component.label} .description=${component.description} .required=${component.required}></dees-label>
|
<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>
|
<dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user