diff --git a/src/views/map-timeline-bases-view.ts b/src/views/map-timeline-bases-view.ts index 5073233..a2cba2e 100644 --- a/src/views/map-timeline-bases-view.ts +++ b/src/views/map-timeline-bases-view.ts @@ -19,6 +19,7 @@ interface TimelineEntry { } type UniquenessMode = 'most-recent' | 'least-recent' | 'all'; +type TimelineGranularity = 'daily' | 'monthly' | 'yearly'; export class MapTimelineBasesView extends MapBasesView { type = MapTimelineBasesViewType; @@ -28,6 +29,7 @@ export class MapTimelineBasesView extends MapBasesView { private dateProperty: BasesPropertyId | null = null; private uniquenessProperty: BasesPropertyId | null = null; private uniquenessMode: UniquenessMode = 'all'; + private granularity: TimelineGranularity = 'daily'; private dateRangeStart: number = 0; private dateRangeEnd: number = Date.now(); private allTimelineEntries: TimelineEntry[] = []; @@ -48,10 +50,27 @@ export class MapTimelineBasesView extends MapBasesView { private createSlider(): void { const sliderContainer = this.mapEl.createDiv({ cls: 'bases-timeline-slider' }); - const dateInputEl = sliderContainer.createEl('input', { + const inputContainer = sliderContainer.createDiv({ cls: 'timeline-input-container' }); + + const dateInputEl = inputContainer.createEl('input', { type: 'text', cls: 'timeline-date-input', - placeholder: 'YYYY-MM-DD', + }); + + const granularitySelect = inputContainer.createEl('select', { + cls: 'timeline-granularity-select', + }); + + const dailyOption = granularitySelect.createEl('option', { value: 'daily', text: 'Daily' }); + const monthlyOption = granularitySelect.createEl('option', { value: 'monthly', text: 'Monthly' }); + const yearlyOption = granularitySelect.createEl('option', { value: 'yearly', text: 'Yearly' }); + + granularitySelect.value = this.granularity; + + granularitySelect.addEventListener('change', () => { + this.granularity = granularitySelect.value as TimelineGranularity; + this.config.set('_granularity', this.granularity); + this.updateDateInput(dateInputEl); }); dateInputEl.addEventListener('change', () => { @@ -101,17 +120,40 @@ export class MapTimelineBasesView extends MapBasesView { } private parseDateInput(dateString: string): number | null { - const match = dateString.match(/^(-?\d+)-(\d{1,2})-(\d{1,2})$/); - if (!match) return null; + // Try full date format (YYYY-MM-DD) + let match = dateString.match(/^(-?\d+)-(\d{1,2})-(\d{1,2})$/); + if (match) { + const year = parseInt(match[1]); + const month = parseInt(match[2]); + const day = parseInt(match[3]); - const year = parseInt(match[1]); - const month = parseInt(match[2]); - const day = parseInt(match[3]); + if (month < 1 || month > 12 || day < 1 || day > 31) return null; - if (month < 1 || month > 12 || day < 1 || day > 31) return null; + const date = new Date(year, month - 1, day); + return isNaN(date.getTime()) ? null : date.getTime(); + } - const date = new Date(year, month - 1, day); - return isNaN(date.getTime()) ? null : date.getTime(); + // Try year-month format (YYYY-MM) + match = dateString.match(/^(-?\d+)-(\d{1,2})$/); + if (match) { + const year = parseInt(match[1]); + const month = parseInt(match[2]); + + if (month < 1 || month > 12) return null; + + const date = new Date(year, month - 1, 1); + return isNaN(date.getTime()) ? null : date.getTime(); + } + + // Try year only format (YYYY) + match = dateString.match(/^(-?\d+)$/); + if (match) { + const year = parseInt(match[1]); + const date = new Date(year, 0, 1); + return isNaN(date.getTime()) ? null : date.getTime(); + } + + return null; } private padNumber(num: number, length: number): string { @@ -128,7 +170,16 @@ export class MapTimelineBasesView extends MapBasesView { const month = this.padNumber(date.getMonth() + 1, 2); const day = this.padNumber(date.getDate(), 2); - dateInputEl.value = `${year}-${month}-${day}`; + if (this.granularity === 'yearly') { + dateInputEl.value = `${year}`; + dateInputEl.placeholder = 'YYYY'; + } else if (this.granularity === 'monthly') { + dateInputEl.value = `${year}-${month}`; + dateInputEl.placeholder = 'YYYY-MM'; + } else { + dateInputEl.value = `${year}-${month}-${day}`; + dateInputEl.placeholder = 'YYYY-MM-DD'; + } } @@ -170,6 +221,11 @@ export class MapTimelineBasesView extends MapBasesView { this.uniquenessMode = modeVal; } + const savedGranularity = this.config.get('_granularity'); + if (savedGranularity === 'daily' || savedGranularity === 'monthly' || savedGranularity === 'yearly') { + this.granularity = savedGranularity; + } + const savedDateRangeEnd = this.config.get('_dateRangeEnd'); if (savedDateRangeEnd && typeof savedDateRangeEnd === 'number') { this.dateRangeEnd = savedDateRangeEnd; diff --git a/styles.css b/styles.css index f30f0ab..2503e1f 100644 --- a/styles.css +++ b/styles.css @@ -376,8 +376,13 @@ gap: 16px; } +.timeline-input-container { + display: flex; + gap: 8px; +} + .timeline-date-input { - width: 100%; + flex: 1; padding: 6px 8px; border: 1px solid var(--background-modifier-border); border-radius: 4px; @@ -386,6 +391,17 @@ font-size: 13px; } +.timeline-granularity-select { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-secondary); + color: var(--text-normal); + font-size: 13px; + cursor: pointer; +} + .timeline-date-display { font-size: 12px; color: var(--text-muted);