- Implemented DeesInputFileupload component with file upload functionality, including drag-and-drop support, file previews, and clear all option. - Developed DeesInputRichtext component featuring a rich text editor with a formatting toolbar, link management, and word count display. - Created demo for DeesInputRichtext showcasing various use cases including basic editing, placeholder text, different heights, and disabled state. - Added styles for both components to ensure a consistent and user-friendly interface. - Introduced types for toolbar buttons in the rich text editor for better type safety and maintainability.
624 lines
18 KiB
TypeScript
624 lines
18 KiB
TypeScript
import {
|
|
customElement,
|
|
type TemplateResult,
|
|
property,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
import { DeesInputBase } from '../dees-input-base.js';
|
|
import { demoFunc } from './demo.js';
|
|
import { datepickerStyles } from './styles.js';
|
|
import { renderDatepicker } from './template.js';
|
|
import type { IDateEvent } from './types.js';
|
|
import '../dees-icon.js';
|
|
import '../dees-label.js';
|
|
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'dees-input-datepicker': DeesInputDatepicker;
|
|
}
|
|
}
|
|
|
|
@customElement('dees-input-datepicker')
|
|
export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
|
public static demo = demoFunc;
|
|
|
|
@property({ type: String })
|
|
public value: string = '';
|
|
|
|
@property({ type: Boolean })
|
|
public enableTime: boolean = false;
|
|
|
|
@property({ type: String })
|
|
public timeFormat: '24h' | '12h' = '24h';
|
|
|
|
@property({ type: Number })
|
|
public minuteIncrement: number = 1;
|
|
|
|
@property({ type: String })
|
|
public dateFormat: string = 'YYYY-MM-DD';
|
|
|
|
@property({ type: String })
|
|
public minDate: string = '';
|
|
|
|
@property({ type: String })
|
|
public maxDate: string = '';
|
|
|
|
@property({ type: Array })
|
|
public disabledDates: string[] = [];
|
|
|
|
@property({ type: Number })
|
|
public weekStartsOn: 0 | 1 = 1; // Default to Monday
|
|
|
|
@property({ type: String })
|
|
public placeholder: string = 'YYYY-MM-DD';
|
|
|
|
@property({ type: Boolean })
|
|
public enableTimezone: boolean = false;
|
|
|
|
@property({ type: String })
|
|
public timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
@property({ type: Array })
|
|
public events: IDateEvent[] = [];
|
|
|
|
@state()
|
|
public isOpened: boolean = false;
|
|
|
|
@state()
|
|
public opensToTop: boolean = false;
|
|
|
|
@state()
|
|
public selectedDate: Date | null = null;
|
|
|
|
@state()
|
|
public viewDate: Date = new Date();
|
|
|
|
@state()
|
|
public selectedHour: number = 0;
|
|
|
|
@state()
|
|
public selectedMinute: number = 0;
|
|
|
|
public static styles = datepickerStyles;
|
|
|
|
|
|
|
|
public getTimezones(): { value: string; label: string }[] {
|
|
// Common timezones with their display names
|
|
return [
|
|
{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
|
|
{ value: 'America/New_York', label: 'Eastern Time (US & Canada)' },
|
|
{ value: 'America/Chicago', label: 'Central Time (US & Canada)' },
|
|
{ value: 'America/Denver', label: 'Mountain Time (US & Canada)' },
|
|
{ value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' },
|
|
{ value: 'America/Phoenix', label: 'Arizona' },
|
|
{ value: 'America/Anchorage', label: 'Alaska' },
|
|
{ value: 'Pacific/Honolulu', label: 'Hawaii' },
|
|
{ value: 'Europe/London', label: 'London' },
|
|
{ value: 'Europe/Paris', label: 'Paris' },
|
|
{ value: 'Europe/Berlin', label: 'Berlin' },
|
|
{ value: 'Europe/Moscow', label: 'Moscow' },
|
|
{ value: 'Asia/Dubai', label: 'Dubai' },
|
|
{ value: 'Asia/Kolkata', label: 'India Standard Time' },
|
|
{ value: 'Asia/Shanghai', label: 'China Standard Time' },
|
|
{ value: 'Asia/Tokyo', label: 'Tokyo' },
|
|
{ value: 'Australia/Sydney', label: 'Sydney' },
|
|
{ value: 'Pacific/Auckland', label: 'Auckland' },
|
|
];
|
|
}
|
|
|
|
public render(): TemplateResult {
|
|
return renderDatepicker(this);
|
|
}
|
|
|
|
|
|
|
|
async connectedCallback() {
|
|
super.connectedCallback();
|
|
this.handleClickOutside = this.handleClickOutside.bind(this);
|
|
}
|
|
|
|
async disconnectedCallback() {
|
|
await super.disconnectedCallback();
|
|
document.removeEventListener('click', this.handleClickOutside);
|
|
}
|
|
|
|
async firstUpdated() {
|
|
// Initialize with empty value if not set
|
|
if (!this.value) {
|
|
this.value = '';
|
|
}
|
|
|
|
// Initialize view date and selected time
|
|
if (this.value) {
|
|
try {
|
|
const date = new Date(this.value);
|
|
if (!isNaN(date.getTime())) {
|
|
this.selectedDate = date;
|
|
this.viewDate = new Date(date);
|
|
this.selectedHour = date.getHours();
|
|
this.selectedMinute = date.getMinutes();
|
|
}
|
|
} catch {
|
|
// Invalid date
|
|
}
|
|
} else {
|
|
const now = new Date();
|
|
this.viewDate = new Date(now);
|
|
this.selectedHour = now.getHours();
|
|
this.selectedMinute = 0;
|
|
}
|
|
}
|
|
|
|
public formatDate(isoString: string): string {
|
|
if (!isoString) return '';
|
|
|
|
try {
|
|
const date = new Date(isoString);
|
|
if (isNaN(date.getTime())) return '';
|
|
|
|
let formatted = this.dateFormat;
|
|
|
|
// Basic date formatting
|
|
const day = date.getDate().toString().padStart(2, '0');
|
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
const year = date.getFullYear().toString();
|
|
|
|
// Replace in correct order to avoid conflicts
|
|
formatted = formatted.replace('YYYY', year);
|
|
formatted = formatted.replace('YY', year.slice(-2));
|
|
formatted = formatted.replace('MM', month);
|
|
formatted = formatted.replace('DD', day);
|
|
|
|
// Time formatting if enabled
|
|
if (this.enableTime) {
|
|
const hours24 = date.getHours();
|
|
const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24;
|
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
const ampm = hours24 >= 12 ? 'PM' : 'AM';
|
|
|
|
if (this.timeFormat === '12h') {
|
|
formatted += ` ${hours12}:${minutes} ${ampm}`;
|
|
} else {
|
|
formatted += ` ${hours24.toString().padStart(2, '0')}:${minutes}`;
|
|
}
|
|
}
|
|
|
|
// Timezone formatting if enabled
|
|
if (this.enableTimezone) {
|
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
timeZoneName: 'short',
|
|
timeZone: this.timezone
|
|
});
|
|
const parts = formatter.formatToParts(date);
|
|
const tzPart = parts.find(part => part.type === 'timeZoneName');
|
|
if (tzPart) {
|
|
formatted += ` ${tzPart.value}`;
|
|
}
|
|
}
|
|
|
|
return formatted;
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
private handleClickOutside = (event: MouseEvent) => {
|
|
const path = event.composedPath();
|
|
if (!path.includes(this)) {
|
|
this.isOpened = false;
|
|
document.removeEventListener('click', this.handleClickOutside);
|
|
}
|
|
};
|
|
|
|
public async toggleCalendar(): Promise<void> {
|
|
if (this.disabled) return;
|
|
|
|
this.isOpened = !this.isOpened;
|
|
|
|
if (this.isOpened) {
|
|
// Check available space and set position
|
|
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
|
const rect = inputContainer.getBoundingClientRect();
|
|
const spaceBelow = window.innerHeight - rect.bottom;
|
|
const spaceAbove = rect.top;
|
|
|
|
// Determine if we should open upwards (approximate height of 400px)
|
|
this.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
|
|
|
// Add click outside listener
|
|
setTimeout(() => {
|
|
document.addEventListener('click', this.handleClickOutside);
|
|
}, 0);
|
|
} else {
|
|
document.removeEventListener('click', this.handleClickOutside);
|
|
}
|
|
}
|
|
|
|
public getDaysInMonth(): Date[] {
|
|
const year = this.viewDate.getFullYear();
|
|
const month = this.viewDate.getMonth();
|
|
const firstDay = new Date(year, month, 1);
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
const days: Date[] = [];
|
|
|
|
// Adjust for week start
|
|
const startOffset = this.weekStartsOn === 1
|
|
? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
|
|
: firstDay.getDay();
|
|
|
|
// Add days from previous month
|
|
for (let i = startOffset; i > 0; i--) {
|
|
days.push(new Date(year, month, 1 - i));
|
|
}
|
|
|
|
// Add days of current month
|
|
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
days.push(new Date(year, month, i));
|
|
}
|
|
|
|
// Add days from next month to complete the grid (6 rows)
|
|
const remainingDays = 42 - days.length;
|
|
for (let i = 1; i <= remainingDays; i++) {
|
|
days.push(new Date(year, month + 1, i));
|
|
}
|
|
|
|
return days;
|
|
}
|
|
|
|
public isToday(date: Date): boolean {
|
|
const today = new Date();
|
|
return date.getDate() === today.getDate() &&
|
|
date.getMonth() === today.getMonth() &&
|
|
date.getFullYear() === today.getFullYear();
|
|
}
|
|
|
|
public isSelected(date: Date): boolean {
|
|
if (!this.selectedDate) return false;
|
|
return date.getDate() === this.selectedDate.getDate() &&
|
|
date.getMonth() === this.selectedDate.getMonth() &&
|
|
date.getFullYear() === this.selectedDate.getFullYear();
|
|
}
|
|
|
|
public isDisabled(date: Date): boolean {
|
|
// Check min date
|
|
if (this.minDate) {
|
|
const min = new Date(this.minDate);
|
|
if (date < min) return true;
|
|
}
|
|
|
|
// Check max date
|
|
if (this.maxDate) {
|
|
const max = new Date(this.maxDate);
|
|
if (date > max) return true;
|
|
}
|
|
|
|
// Check disabled dates
|
|
if (this.disabledDates && this.disabledDates.length > 0) {
|
|
return this.disabledDates.some(disabledStr => {
|
|
try {
|
|
const disabled = new Date(disabledStr);
|
|
return date.getDate() === disabled.getDate() &&
|
|
date.getMonth() === disabled.getMonth() &&
|
|
date.getFullYear() === disabled.getFullYear();
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public getEventsForDate(date: Date): IDateEvent[] {
|
|
if (!this.events || this.events.length === 0) return [];
|
|
|
|
const dateStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
|
return this.events.filter(event => event.date === dateStr);
|
|
}
|
|
|
|
public selectDate(date: Date): void {
|
|
this.selectedDate = new Date(
|
|
date.getFullYear(),
|
|
date.getMonth(),
|
|
date.getDate(),
|
|
this.selectedHour,
|
|
this.selectedMinute
|
|
);
|
|
|
|
this.value = this.formatValueWithTimezone(this.selectedDate);
|
|
this.changeSubject.next(this);
|
|
|
|
if (!this.enableTime) {
|
|
this.isOpened = false;
|
|
}
|
|
}
|
|
|
|
public selectToday(): void {
|
|
const today = new Date();
|
|
this.selectedDate = today;
|
|
this.viewDate = new Date(today);
|
|
this.selectedHour = today.getHours();
|
|
this.selectedMinute = today.getMinutes();
|
|
|
|
this.value = this.formatValueWithTimezone(this.selectedDate);
|
|
this.changeSubject.next(this);
|
|
|
|
if (!this.enableTime) {
|
|
this.isOpened = false;
|
|
}
|
|
}
|
|
|
|
public clear(): void {
|
|
this.value = '';
|
|
this.selectedDate = null;
|
|
this.changeSubject.next(this);
|
|
this.isOpened = false;
|
|
}
|
|
|
|
public previousMonth(): void {
|
|
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
|
|
}
|
|
|
|
public nextMonth(): void {
|
|
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
|
|
}
|
|
|
|
public handleHourInput(e: InputEvent): void {
|
|
const input = e.target as HTMLInputElement;
|
|
let value = parseInt(input.value) || 0;
|
|
|
|
if (this.timeFormat === '12h') {
|
|
value = Math.max(1, Math.min(12, value));
|
|
// Convert to 24h format
|
|
if (this.selectedHour >= 12 && value !== 12) {
|
|
this.selectedHour = value + 12;
|
|
} else if (this.selectedHour < 12 && value === 12) {
|
|
this.selectedHour = 0;
|
|
} else {
|
|
this.selectedHour = value;
|
|
}
|
|
} else {
|
|
this.selectedHour = Math.max(0, Math.min(23, value));
|
|
}
|
|
|
|
this.updateSelectedDateTime();
|
|
}
|
|
|
|
public handleMinuteInput(e: InputEvent): void {
|
|
const input = e.target as HTMLInputElement;
|
|
let value = parseInt(input.value) || 0;
|
|
value = Math.max(0, Math.min(59, value));
|
|
|
|
if (this.minuteIncrement && this.minuteIncrement > 1) {
|
|
value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
|
|
}
|
|
|
|
this.selectedMinute = value;
|
|
this.updateSelectedDateTime();
|
|
}
|
|
|
|
public setAMPM(period: 'am' | 'pm'): void {
|
|
if (period === 'am' && this.selectedHour >= 12) {
|
|
this.selectedHour -= 12;
|
|
} else if (period === 'pm' && this.selectedHour < 12) {
|
|
this.selectedHour += 12;
|
|
}
|
|
this.updateSelectedDateTime();
|
|
}
|
|
|
|
private updateSelectedDateTime(): void {
|
|
if (this.selectedDate) {
|
|
this.selectedDate = new Date(
|
|
this.selectedDate.getFullYear(),
|
|
this.selectedDate.getMonth(),
|
|
this.selectedDate.getDate(),
|
|
this.selectedHour,
|
|
this.selectedMinute
|
|
);
|
|
this.value = this.formatValueWithTimezone(this.selectedDate);
|
|
this.changeSubject.next(this);
|
|
}
|
|
}
|
|
|
|
public handleTimezoneChange(e: Event): void {
|
|
const select = e.target as HTMLSelectElement;
|
|
this.timezone = select.value;
|
|
this.updateSelectedDateTime();
|
|
}
|
|
|
|
private formatValueWithTimezone(date: Date): string {
|
|
if (!this.enableTimezone) {
|
|
return date.toISOString();
|
|
}
|
|
|
|
// Format the date with timezone offset
|
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false,
|
|
timeZone: this.timezone,
|
|
timeZoneName: 'short'
|
|
});
|
|
|
|
const parts = formatter.formatToParts(date);
|
|
const dateParts: any = {};
|
|
parts.forEach(part => {
|
|
dateParts[part.type] = part.value;
|
|
});
|
|
|
|
// Create ISO-like format with timezone
|
|
const isoString = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
|
|
|
|
// Get timezone offset
|
|
const tzOffset = this.getTimezoneOffset(date, this.timezone);
|
|
return `${isoString}${tzOffset}`;
|
|
}
|
|
|
|
private getTimezoneOffset(date: Date, timezone: string): string {
|
|
// Create a date in the target timezone
|
|
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
|
|
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
|
|
|
const offsetMinutes = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60);
|
|
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
|
|
const minutes = Math.abs(offsetMinutes) % 60;
|
|
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
|
|
return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
public handleKeydown(e: KeyboardEvent): void {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
this.toggleCalendar();
|
|
} else if (e.key === 'Escape' && this.isOpened) {
|
|
e.preventDefault();
|
|
this.isOpened = false;
|
|
}
|
|
}
|
|
|
|
public clearValue(e: Event): void {
|
|
e.stopPropagation();
|
|
this.value = '';
|
|
this.selectedDate = null;
|
|
this.changeSubject.next(this);
|
|
}
|
|
|
|
public handleManualInput(e: InputEvent): void {
|
|
const input = e.target as HTMLInputElement;
|
|
const inputValue = input.value.trim();
|
|
|
|
if (!inputValue) {
|
|
// Clear the value if input is empty
|
|
this.value = '';
|
|
this.selectedDate = null;
|
|
return;
|
|
}
|
|
|
|
const parsedDate = this.parseManualDate(inputValue);
|
|
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
|
// Update internal state without triggering re-render of input
|
|
this.value = parsedDate.toISOString();
|
|
this.selectedDate = parsedDate;
|
|
this.viewDate = new Date(parsedDate);
|
|
this.selectedHour = parsedDate.getHours();
|
|
this.selectedMinute = parsedDate.getMinutes();
|
|
this.changeSubject.next(this);
|
|
}
|
|
}
|
|
|
|
public handleInputBlur(e: FocusEvent): void {
|
|
const input = e.target as HTMLInputElement;
|
|
const inputValue = input.value.trim();
|
|
|
|
if (!inputValue) {
|
|
this.value = '';
|
|
this.selectedDate = null;
|
|
this.changeSubject.next(this);
|
|
return;
|
|
}
|
|
|
|
const parsedDate = this.parseManualDate(inputValue);
|
|
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
|
this.value = parsedDate.toISOString();
|
|
this.selectedDate = parsedDate;
|
|
this.viewDate = new Date(parsedDate);
|
|
this.selectedHour = parsedDate.getHours();
|
|
this.selectedMinute = parsedDate.getMinutes();
|
|
this.changeSubject.next(this);
|
|
// Update the input with formatted date
|
|
input.value = this.formatDate(this.value);
|
|
} else {
|
|
// Revert to previous valid value on blur if parsing failed
|
|
input.value = this.formatDate(this.value);
|
|
}
|
|
}
|
|
|
|
private parseManualDate(input: string): Date | null {
|
|
if (!input) return null;
|
|
|
|
// Split date and time parts if present
|
|
const parts = input.split(' ');
|
|
let datePart = parts[0];
|
|
let timePart = parts[1] || '';
|
|
|
|
let parsedDate: Date | null = null;
|
|
|
|
// Try different date formats
|
|
// Format 1: YYYY-MM-DD (ISO-like)
|
|
const isoMatch = datePart.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
if (isoMatch) {
|
|
const [_, year, month, day] = isoMatch;
|
|
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
}
|
|
|
|
// Format 2: DD.MM.YYYY (European)
|
|
if (!parsedDate) {
|
|
const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
|
if (euMatch) {
|
|
const [_, day, month, year] = euMatch;
|
|
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
}
|
|
}
|
|
|
|
// Format 3: MM/DD/YYYY (US)
|
|
if (!parsedDate) {
|
|
const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
if (usMatch) {
|
|
const [_, month, day, year] = usMatch;
|
|
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
}
|
|
}
|
|
|
|
// If no date was parsed, return null
|
|
if (!parsedDate || isNaN(parsedDate.getTime())) {
|
|
return null;
|
|
}
|
|
|
|
// Parse time if present (HH:MM format)
|
|
if (timePart) {
|
|
const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/);
|
|
if (timeMatch) {
|
|
const [_, hours, minutes] = timeMatch;
|
|
parsedDate.setHours(parseInt(hours));
|
|
parsedDate.setMinutes(parseInt(minutes));
|
|
}
|
|
} else if (!this.enableTime) {
|
|
// If time is not enabled and not provided, use current time
|
|
const now = new Date();
|
|
parsedDate.setHours(now.getHours());
|
|
parsedDate.setMinutes(now.getMinutes());
|
|
parsedDate.setSeconds(0);
|
|
parsedDate.setMilliseconds(0);
|
|
}
|
|
|
|
return parsedDate;
|
|
}
|
|
|
|
public getValue(): string {
|
|
return this.value;
|
|
}
|
|
|
|
public setValue(value: string): void {
|
|
this.value = value;
|
|
if (value) {
|
|
try {
|
|
const date = new Date(value);
|
|
if (!isNaN(date.getTime())) {
|
|
this.selectedDate = date;
|
|
this.viewDate = new Date(date);
|
|
this.selectedHour = date.getHours();
|
|
this.selectedMinute = date.getMinutes();
|
|
}
|
|
} catch {
|
|
// Invalid date
|
|
}
|
|
}
|
|
}
|
|
} |