mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
date navigator
This commit is contained in:
parent
cba7bda90a
commit
9c1020b8d3
19 changed files with 713 additions and 236 deletions
19
main.ts
19
main.ts
|
|
@ -9,11 +9,12 @@ import {
|
||||||
import { DBService } from "./src/DBService";
|
import { DBService } from "./src/DBService";
|
||||||
import { SQLiteDBSettingTab } from "./src/settingTab";
|
import { SQLiteDBSettingTab } from "./src/settingTab";
|
||||||
import { inspectTableStructure, convertEntriesInNotes } from "./src/commands";
|
import { inspectTableStructure, convertEntriesInNotes } from "./src/commands";
|
||||||
import { processSqlBlock, processSqlChartBlock, renderDatePicker } from "./src/codeblocks";
|
import { processSqlBlock, processSqlChartBlock, DateNavigatorRenderer } from "./src/codeblocks";
|
||||||
import { pickTableName } from "./src/helpers";
|
import { pickTableName } from "./src/helpers";
|
||||||
import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types";
|
import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types";
|
||||||
|
|
||||||
import { injectDatePickerStyles } from "src/styles/datePickerInject";
|
import { injectDatePickerStyles } from "src/styles/datePickerInject";
|
||||||
|
import { injectDateNavigatorStyles, removeDateNavigatorStyles } from './src/styles/dateNavigationInject';
|
||||||
|
|
||||||
import { registerHabitCounter } from "./src/webcomponents/HabitCounter/registerHabitCounter";
|
import { registerHabitCounter } from "./src/webcomponents/HabitCounter/registerHabitCounter";
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@ export default class SQLiteDBPlugin extends Plugin {
|
||||||
await this.openDatabase();
|
await this.openDatabase();
|
||||||
|
|
||||||
injectDatePickerStyles();
|
injectDatePickerStyles();
|
||||||
|
injectDateNavigatorStyles();
|
||||||
|
|
||||||
this.registerMarkdownPostProcessor((el, ctx) => {
|
this.registerMarkdownPostProcessor((el, ctx) => {
|
||||||
registerHabitCounter(el, this.dbService);
|
registerHabitCounter(el, this.dbService);
|
||||||
|
|
@ -73,18 +75,19 @@ export default class SQLiteDBPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.registerMarkdownCodeBlockProcessor(
|
this.registerMarkdownCodeBlockProcessor(
|
||||||
"date-picker",
|
"date-header",
|
||||||
async (source: string, el: HTMLElement) => {
|
(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||||
renderDatePicker(el, this.app);
|
ctx.addChild(new DateNavigatorRenderer(el, this.app));
|
||||||
}
|
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.addSettingTab(new SQLiteDBSettingTab(this.app, this));
|
this.addSettingTab(new SQLiteDBSettingTab(this.app, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
console.log("Unloading SQLiteDBPlugin...");
|
removeDateNavigatorStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openDatabase(forceReload = true) {
|
private async openDatabase(forceReload = true) {
|
||||||
|
|
|
||||||
|
|
@ -33,24 +33,19 @@ export class DBService {
|
||||||
// Only reload if forced or not already loaded
|
// Only reload if forced or not already loaded
|
||||||
if (!this.db || forceReload) {
|
if (!this.db || forceReload) {
|
||||||
try {
|
try {
|
||||||
console.log("DBService: Initializing SQL.js and loading DB...");
|
|
||||||
this.SQL = await initSqlJs({
|
this.SQL = await initSqlJs({
|
||||||
locateFile: (file) => `${basePath}/${this.app.vault.configDir}/plugins/sqlite-db/${file}`,
|
locateFile: (file) => `${basePath}/${this.app.vault.configDir}/plugins/sqlite-db/${file}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`DBService: Reading DB file from: ${settings.dbFilePath}`);
|
|
||||||
const fileBuffer = readFileSync(settings.dbFilePath);
|
const fileBuffer = readFileSync(settings.dbFilePath);
|
||||||
this.db = new this.SQL.Database(fileBuffer);
|
this.db = new this.SQL.Database(fileBuffer);
|
||||||
console.log("DBService: Database loaded into memory.");
|
|
||||||
|
|
||||||
// Check if we have at least one table
|
// Check if we have at least one table
|
||||||
const result = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1;");
|
const result = this.db.exec("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1;");
|
||||||
if (result?.[0]?.values?.[0]?.[0]) {
|
if (result?.[0]?.values?.[0]?.[0]) {
|
||||||
new Notice(`DB Loaded.`);
|
new Notice(`DB Loaded.`);
|
||||||
console.log("DBService: DB Loaded successfully.");
|
|
||||||
} else {
|
} else {
|
||||||
new Notice("DB Loaded, but found no tables.");
|
new Notice("DB Loaded, but found no tables.");
|
||||||
console.log("DBService: DB Loaded, but found no tables.");
|
|
||||||
}
|
}
|
||||||
return true; // Indicate success
|
return true; // Indicate success
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -61,8 +56,7 @@ export class DBService {
|
||||||
return false; // Indicate failure
|
return false; // Indicate failure
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("DBService: Database already loaded.");
|
return true; // Already loaded
|
||||||
return true; // Already loaded
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { App } from "obsidian";
|
||||||
|
import { PluginState } from "src/pluginState";
|
||||||
|
import { DateNavigatorOptions, DateNavigatorDOMElements, NavigationPeriod } from "./dateNavigator.types";
|
||||||
|
import { createPrevHandler, createNextHandler, createOpenModalHandler } from "./eventHandlers/dateNavigationHandlers";
|
||||||
|
import { buildDateNavigatorDOM, updateDateNavigatorDisplay } from "./dom/buildDateNavigatorDOM";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and manages the interactive Date Navigator header.
|
||||||
|
* Uses pluginState to get/set the current date.
|
||||||
|
* Updates its display when the date changes.
|
||||||
|
*/
|
||||||
|
export class DateNavigator {
|
||||||
|
private app: App;
|
||||||
|
private pluginState: PluginState;
|
||||||
|
private containerEl: HTMLElement;
|
||||||
|
private period: NavigationPeriod;
|
||||||
|
private elements: DateNavigatorDOMElements | null = null;
|
||||||
|
|
||||||
|
constructor(options: DateNavigatorOptions) {
|
||||||
|
this.app = options.app;
|
||||||
|
this.pluginState = options.pluginState;
|
||||||
|
this.containerEl = options.containerEl;
|
||||||
|
this.period = options.period ?? 'day'; // Default to day navigation
|
||||||
|
|
||||||
|
if (!this.app || !this.pluginState || !this.containerEl) {
|
||||||
|
throw new Error("DateNavigator missing required options (app, pluginState, containerEl).");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._buildUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the initial UI */
|
||||||
|
private _buildUI(): void {
|
||||||
|
//~ Create bound handlers first
|
||||||
|
const handlers = {
|
||||||
|
handlePrev: createPrevHandler(this.pluginState, this.period, this.app, this._updateDisplay),
|
||||||
|
handleNext: createNextHandler(this.pluginState, this.period, this.app, this._updateDisplay),
|
||||||
|
handleOpenModal: createOpenModalHandler(this.pluginState, this.app, this._updateDisplay),
|
||||||
|
};
|
||||||
|
//~ Build DOM and store element references
|
||||||
|
this.elements = buildDateNavigatorDOM(this.containerEl, this.pluginState.selectedDate, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the displayed date. Bound method for callbacks. */
|
||||||
|
private _updateDisplay = (newIsoDate: string): void => {
|
||||||
|
if (this.elements?.dateDisplay) {
|
||||||
|
updateDateNavigatorDisplay(this.elements.dateDisplay, newIsoDate);
|
||||||
|
} else {
|
||||||
|
console.error("[DateNavigator] Cannot update display, dateDisplay element reference is missing.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Cleans up the component (e.g., remove listeners, clear container) */
|
||||||
|
public destroy(): void {
|
||||||
|
this.containerEl.empty();
|
||||||
|
this.elements = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// src/components/dateNavigator/dateNavigator.types.ts
|
||||||
|
import { App } from "obsidian";
|
||||||
|
import { pluginState } from "src/pluginState";
|
||||||
|
|
||||||
|
//? Defines the period types the navigator can handle (currently only 'day')
|
||||||
|
export type NavigationPeriod = 'day'; //todo: Add 'week', 'month', 'year' later
|
||||||
|
|
||||||
|
//? Configuration options for the DateNavigator
|
||||||
|
export interface DateNavigatorOptions {
|
||||||
|
app: App;
|
||||||
|
pluginState: typeof pluginState;
|
||||||
|
containerEl: HTMLElement; // Where to render the navigator
|
||||||
|
period?: NavigationPeriod; // Defaults to 'day'
|
||||||
|
dateFormat?: string; // todo: Allow custom date formats later
|
||||||
|
}
|
||||||
|
|
||||||
|
//? References to the DOM elements created by the navigator builder
|
||||||
|
export interface DateNavigatorDOMElements {
|
||||||
|
wrapper: HTMLElement;
|
||||||
|
navRow: HTMLElement;
|
||||||
|
prevButton: HTMLButtonElement;
|
||||||
|
nextButton: HTMLButtonElement;
|
||||||
|
dateDisplay: HTMLElement; // The H1 or span showing the date
|
||||||
|
openModalButton: HTMLButtonElement;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// src/components/dateNavigator/dom/buildDateNavigatorDOM.ts
|
||||||
|
import { formatDateForDisplay, parseDateISO } from "../../datePicker/datePickerUtils";
|
||||||
|
import type { DateNavigatorDOMElements } from "../dateNavigator.types"; // Type only
|
||||||
|
|
||||||
|
//? Structure for the handlers needed by the builder
|
||||||
|
export interface DateNavigatorHandlers {
|
||||||
|
handlePrev: () => void;
|
||||||
|
handleNext: () => void;
|
||||||
|
handleOpenModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** //? Builds the DOM for the date navigator header. */
|
||||||
|
export function buildDateNavigatorDOM(
|
||||||
|
containerEl: HTMLElement,
|
||||||
|
initialIsoDate: string,
|
||||||
|
handlers: DateNavigatorHandlers
|
||||||
|
): DateNavigatorDOMElements {
|
||||||
|
containerEl.empty(); // Clear container first
|
||||||
|
const wrapper = containerEl.createDiv({ cls: "date-navigator-wrapper" });
|
||||||
|
const navRow = wrapper.createDiv({ cls: "date-nav-row" });
|
||||||
|
|
||||||
|
// --- Navigation Buttons ---
|
||||||
|
const prevButton = navRow.createEl("button", { text: "←", cls: "clickable-icon date-nav-button date-nav-prev" });
|
||||||
|
prevButton.setAttribute("aria-label", "Previous Day"); //todo: Make label dynamic based on period
|
||||||
|
prevButton.addEventListener("click", handlers.handlePrev);
|
||||||
|
|
||||||
|
// --- Date Display (H1) ---
|
||||||
|
const dateDisplay = navRow.createEl("h1", { cls: "date-navigator-display" });
|
||||||
|
const initialDateObj = parseDateISO(initialIsoDate);
|
||||||
|
dateDisplay.textContent = initialDateObj ? formatDateForDisplay(initialDateObj) : "Invalid Date";
|
||||||
|
|
||||||
|
const nextButton = navRow.createEl("button", { text: "→", cls: "clickable-icon date-nav-button date-nav-next" });
|
||||||
|
nextButton.setAttribute("aria-label", "Next Day"); //todo: Make label dynamic
|
||||||
|
nextButton.addEventListener("click", handlers.handleNext);
|
||||||
|
|
||||||
|
// --- Open Modal Button ---
|
||||||
|
//? Placed below the H1 for structure
|
||||||
|
const openModalButton = wrapper.createEl("button", { text: "Select Date", cls: "date-nav-open-modal" });
|
||||||
|
openModalButton.addEventListener("click", handlers.handleOpenModal);
|
||||||
|
|
||||||
|
return { wrapper, navRow, prevButton, nextButton, dateDisplay, openModalButton };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the displayed date text in the navigator. */
|
||||||
|
export function updateDateNavigatorDisplay(dateDisplayElement: HTMLElement, newIsoDate: string): void {
|
||||||
|
const dateObj = parseDateISO(newIsoDate);
|
||||||
|
const displayString = dateObj ? formatDateForDisplay(dateObj) : "Invalid Date";
|
||||||
|
if (dateDisplayElement) {
|
||||||
|
dateDisplayElement.textContent = displayString;
|
||||||
|
} else {
|
||||||
|
console.error("[DateNavDOM] Cannot update display, element not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
// src/components/dateNavigator/eventHandlers/dateNavigationHandlers.ts
|
||||||
|
import { App } from "obsidian";
|
||||||
|
import { DatePickerModal } from "../../datePicker/DatePickerModal";
|
||||||
|
import { PluginState } from "src/pluginState";
|
||||||
|
import { parseDateISO, formatDateISO } from "../../datePicker/datePickerUtils";
|
||||||
|
import { NavigationPeriod } from "../dateNavigator.types";
|
||||||
|
|
||||||
|
/** Updates the global plugin state with the new date and triggers necessary updates */
|
||||||
|
function updateGlobalDate(newDate: Date, pluginState: PluginState, app: App): void {
|
||||||
|
const newIsoDate = formatDateISO(newDate);
|
||||||
|
if (pluginState.selectedDate !== newIsoDate) {
|
||||||
|
pluginState.selectedDate = newIsoDate;
|
||||||
|
// Optional: Force re-render if needed, maybe make this configurable
|
||||||
|
// app.workspace.getActiveViewOfType(MarkdownView)?.previewMode?.rerender(true);
|
||||||
|
}
|
||||||
|
//todo: Could add dispatchEvent here for more reactive updates elsewhere
|
||||||
|
}
|
||||||
|
|
||||||
|
/** //? Creates the handler for the "previous" button click */
|
||||||
|
export function createPrevHandler(
|
||||||
|
pluginState: PluginState,
|
||||||
|
period: NavigationPeriod,
|
||||||
|
app: App,
|
||||||
|
//? Callback to update the display text after changing the date
|
||||||
|
updateDisplayCallback: (newIsoDate: string) => void
|
||||||
|
): () => void {
|
||||||
|
return () => {
|
||||||
|
const currentDate = parseDateISO(pluginState.selectedDate) ?? new Date(); // Default to today on parse error
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
currentDate.setUTCDate(currentDate.getUTCDate() - 1);
|
||||||
|
break;
|
||||||
|
//todo: Implement 'week', 'month', etc.
|
||||||
|
default:
|
||||||
|
console.warn(`[DateNavHandlers] Unsupported period for prev navigation: ${period}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateGlobalDate(currentDate, pluginState, app);
|
||||||
|
updateDisplayCallback(formatDateISO(currentDate)); // Update local display
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates the handler for the "next" button click */
|
||||||
|
export function createNextHandler(
|
||||||
|
pluginState: PluginState,
|
||||||
|
period: NavigationPeriod,
|
||||||
|
app: App,
|
||||||
|
updateDisplayCallback: (newIsoDate: string) => void
|
||||||
|
): () => void {
|
||||||
|
return () => {
|
||||||
|
const currentDate = parseDateISO(pluginState.selectedDate) ?? new Date();
|
||||||
|
//& console.log(`[DateNavHandlers] Next clicked. Current: ${formatDateISO(currentDate)}, Period: ${period}`);
|
||||||
|
switch (period) {
|
||||||
|
case 'day':
|
||||||
|
currentDate.setUTCDate(currentDate.getUTCDate() + 1);
|
||||||
|
break;
|
||||||
|
//todo: Implement 'week', 'month', etc.
|
||||||
|
default:
|
||||||
|
console.warn(`[DateNavHandlers] Unsupported period for next navigation: ${period}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateGlobalDate(currentDate, pluginState, app);
|
||||||
|
updateDisplayCallback(formatDateISO(currentDate));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates the handler for the "open modal" button click */
|
||||||
|
export function createOpenModalHandler(
|
||||||
|
pluginState: PluginState,
|
||||||
|
app: App,
|
||||||
|
updateDisplayCallback: (newIsoDate: string) => void
|
||||||
|
): () => void {
|
||||||
|
return () => {
|
||||||
|
new DatePickerModal(app, pluginState.selectedDate, (newIsoDate) => {
|
||||||
|
//? This is the callback executed when the modal confirms a selection
|
||||||
|
//? No need to call updateGlobalDate here, modal already did via pluginState setter
|
||||||
|
updateDisplayCallback(newIsoDate);
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { Modal, App, Notice } from "obsidian";
|
||||||
|
import { buildDatePickerHeader } from "./dom/buildDatePickerHeader";
|
||||||
|
import { buildWeekdayHeaders } from "./dom/buildWeekdayHeaders";
|
||||||
|
import { buildCalendarGrid } from "./dom/buildCalendarGrid";
|
||||||
|
import { buildDatePickerFooter } from "./dom/buildDatePickerFooter";
|
||||||
|
import { formatDateISO, parseDateISO } from "./datePickerUtils";
|
||||||
|
|
||||||
|
export class DatePickerModal extends Modal {
|
||||||
|
private selectedDateObj: Date; //? Internal state as Date object
|
||||||
|
private currentDisplayMonth: Date; //? Month being displayed in the calendar
|
||||||
|
private today: Date; //? Today's date (normalized)
|
||||||
|
|
||||||
|
//? Callback to notify the parent component of the selection
|
||||||
|
private onSelectCallback: (newIsoDate: string) => void;
|
||||||
|
|
||||||
|
constructor(app: App, currentIsoDate: string, onSelect: (newIsoDate: string) => void) {
|
||||||
|
super(app);
|
||||||
|
this.onSelectCallback = onSelect;
|
||||||
|
|
||||||
|
const initialDate = parseDateISO(currentIsoDate);
|
||||||
|
//? Ensure selectedDateObj is always a valid Date object, default to today if parse fails
|
||||||
|
this.selectedDateObj = initialDate && !isNaN(initialDate.getTime()) ? initialDate : new Date();
|
||||||
|
//? Set time to 00:00:00 UTC to compare dates easily
|
||||||
|
this.selectedDateObj.setUTCHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
this.today = new Date();
|
||||||
|
this.today.setUTCHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
//? Start display month from the selected date's month
|
||||||
|
this.currentDisplayMonth = new Date(Date.UTC(this.selectedDateObj.getUTCFullYear(), this.selectedDateObj.getUTCMonth(), 1));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
this.contentEl.addClass("custom-datepicker-modal-content");
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty(); // Clear previous content
|
||||||
|
|
||||||
|
// --- Build Header ---
|
||||||
|
const headerEl = buildDatePickerHeader(
|
||||||
|
this.currentDisplayMonth,
|
||||||
|
this._handlePrevMonth, // Pass handler references
|
||||||
|
this._handleNextMonth
|
||||||
|
);
|
||||||
|
contentEl.appendChild(headerEl);
|
||||||
|
|
||||||
|
// --- Build Weekday Headers ---
|
||||||
|
const weekdaysEl = buildWeekdayHeaders(true); // Include week number header
|
||||||
|
contentEl.appendChild(weekdaysEl);
|
||||||
|
|
||||||
|
// --- Build Calendar Grid ---
|
||||||
|
const gridEl = buildCalendarGrid(
|
||||||
|
this.currentDisplayMonth,
|
||||||
|
this.selectedDateObj,
|
||||||
|
this.today,
|
||||||
|
this._handleDateSelect // Pass date selection handler
|
||||||
|
);
|
||||||
|
contentEl.appendChild(gridEl);
|
||||||
|
|
||||||
|
// --- Build Footer ---
|
||||||
|
const footerEl = buildDatePickerFooter(
|
||||||
|
this.selectedDateObj,
|
||||||
|
this._confirmSelection // Pass confirmation handler
|
||||||
|
);
|
||||||
|
contentEl.appendChild(footerEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Handlers passed to builders ---
|
||||||
|
|
||||||
|
private _handlePrevMonth = (): void => {
|
||||||
|
this.currentDisplayMonth.setUTCMonth(this.currentDisplayMonth.getUTCMonth() - 1);
|
||||||
|
this.render(); // Re-render the whole modal
|
||||||
|
};
|
||||||
|
|
||||||
|
private _handleNextMonth = (): void => {
|
||||||
|
this.currentDisplayMonth.setUTCMonth(this.currentDisplayMonth.getUTCMonth() + 1);
|
||||||
|
this.render(); // Re-render the whole modal
|
||||||
|
};
|
||||||
|
|
||||||
|
private _handleDateSelect = (selected: Date): void => {
|
||||||
|
this.selectedDateObj = selected; // Update internal state
|
||||||
|
//? Re-render to update selection highlight and footer display
|
||||||
|
//? Alternatively, could update only specific elements for performance
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
private _confirmSelection = (): void => {
|
||||||
|
const isoDate = formatDateISO(this.selectedDateObj);
|
||||||
|
this.onSelectCallback(isoDate); // Notify parent component
|
||||||
|
new Notice(`Date set to ${isoDate}`);
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
this.contentEl.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/** Calculates the ISO 8601 week number for a given date. */
|
||||||
|
export function getISOWeekNumber(date: Date): number {
|
||||||
|
const temp = new Date(date.getTime());
|
||||||
|
temp.setHours(0, 0, 0, 0);
|
||||||
|
// Thursday in current week decides the year.
|
||||||
|
temp.setDate(temp.getDate() + 3 - ((temp.getDay() + 6) % 7));
|
||||||
|
const week1 = new Date(temp.getFullYear(), 0, 4);
|
||||||
|
return (
|
||||||
|
1 +
|
||||||
|
Math.round(
|
||||||
|
((temp.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7 // Adjusted calculation slightly
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Formats a Date object into YYYY-MM-DD string */
|
||||||
|
export function formatDateISO(date: Date): string {
|
||||||
|
if (!date || isNaN(date.getTime())) {
|
||||||
|
return ""; //? Handle invalid date
|
||||||
|
}
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a YYYY-MM-DD string into a Date object (at UTC midnight) */
|
||||||
|
export function parseDateISO(dateString: string): Date | null {
|
||||||
|
if (!dateString || !/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//? Parse as UTC to avoid timezone issues affecting the date part
|
||||||
|
const parts = dateString.split('-').map(Number);
|
||||||
|
const date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Formats a date for display (e.g., "January 15, 2024") */
|
||||||
|
export function formatDateForDisplay(date: Date): string {
|
||||||
|
if (!date || isNaN(date.getTime())) return "Invalid Date";
|
||||||
|
return date.toLocaleDateString(undefined, { // Use user's locale
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
// timeZone: 'UTC'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { getISOWeekNumber } from "../datePickerUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the main grid of days for the calendar month.
|
||||||
|
* @param currentDisplayMonth - The month/year being displayed (Date object, 1st of month UTC).
|
||||||
|
* @param selectedDateObj - The currently selected date (Date object, normalized to UTC midnight).
|
||||||
|
* @param today - Today's date (Date object, normalized to UTC midnight).
|
||||||
|
* @param onDateSelect - Callback function executed when a day button is clicked, passing the selected Date object.
|
||||||
|
* @returns The HTMLElement representing the calendar grid.
|
||||||
|
*/
|
||||||
|
export function buildCalendarGrid(
|
||||||
|
currentDisplayMonth: Date,
|
||||||
|
selectedDateObj: Date,
|
||||||
|
today: Date,
|
||||||
|
onDateSelect: (selected: Date) => void
|
||||||
|
): HTMLElement {
|
||||||
|
const grid = document.createElement("div");
|
||||||
|
grid.className = "calendar-grid"; // Grid container
|
||||||
|
|
||||||
|
const year = currentDisplayMonth.getUTCFullYear();
|
||||||
|
const month = currentDisplayMonth.getUTCMonth();
|
||||||
|
|
||||||
|
//? Day of the week for the 1st of the month (0=Sun, 6=Sat). Use UTC methods.
|
||||||
|
const firstDayWeekday = new Date(Date.UTC(year, month, 1)).getUTCDay();
|
||||||
|
//? Calculate offset for Monday start (0=Mon, 6=Sun)
|
||||||
|
const startOffset = (firstDayWeekday + 6) % 7;
|
||||||
|
|
||||||
|
//? Last day of the *current* month. Use UTC month+1, day 0.
|
||||||
|
const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
||||||
|
|
||||||
|
let dayCounter = 1 - startOffset; // Start day counter including offset for leading empty cells
|
||||||
|
let currentWeekNumber = -1; // Track week number to avoid duplicates per row
|
||||||
|
|
||||||
|
while (dayCounter <= daysInMonth) {
|
||||||
|
// --- Add Week Number Cell (at the start of each visual row) ---
|
||||||
|
const dateForWeekNum = new Date(Date.UTC(year, month, dayCounter > 0 ? dayCounter : 1));
|
||||||
|
const weekNumber = getISOWeekNumber(dateForWeekNum);
|
||||||
|
|
||||||
|
//? Add week number cell at the start of grid row (every 8th element: 1 week# + 7 days)
|
||||||
|
if (grid.children.length % 8 === 0) {
|
||||||
|
const weekEl = document.createElement("div");
|
||||||
|
weekEl.className = "week-number";
|
||||||
|
weekEl.textContent = `${String(weekNumber).padStart(2, "0")}`;
|
||||||
|
grid.appendChild(weekEl);
|
||||||
|
currentWeekNumber = weekNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Add 7 Day Cells for the current week ---
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const cell = document.createElement("div");
|
||||||
|
cell.className = "calendar-day-cell"; // Container for potential button
|
||||||
|
|
||||||
|
if (dayCounter < 1 || dayCounter > daysInMonth) {
|
||||||
|
//? Empty cell (before start or after end of month)
|
||||||
|
cell.classList.add("empty");
|
||||||
|
} else {
|
||||||
|
//? Valid day within the month
|
||||||
|
const currentDate = new Date(Date.UTC(year, month, dayCounter));
|
||||||
|
//? No need to normalize time, UTC dates are already at midnight
|
||||||
|
|
||||||
|
const dayButton = document.createElement("button");
|
||||||
|
dayButton.textContent = String(dayCounter);
|
||||||
|
dayButton.className = "calendar-day";
|
||||||
|
dayButton.setAttribute("aria-label", currentDate.toLocaleDateString(undefined, { // Use local format for aria-label
|
||||||
|
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC'
|
||||||
|
}));
|
||||||
|
|
||||||
|
//? Check if this is the currently selected date
|
||||||
|
if (currentDate.getTime() === selectedDateObj.getTime()) {
|
||||||
|
dayButton.classList.add("selected");
|
||||||
|
dayButton.setAttribute("aria-selected", "true");
|
||||||
|
} else {
|
||||||
|
dayButton.setAttribute("aria-selected", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
//? Check if this is today's date
|
||||||
|
if (currentDate.getTime() === today.getTime()) {
|
||||||
|
dayButton.classList.add("today");
|
||||||
|
}
|
||||||
|
|
||||||
|
//? Attach click handler
|
||||||
|
dayButton.onclick = () => {
|
||||||
|
onDateSelect(currentDate); // Pass the Date object back to the modal's handler
|
||||||
|
};
|
||||||
|
|
||||||
|
cell.appendChild(dayButton);
|
||||||
|
}
|
||||||
|
grid.appendChild(cell);
|
||||||
|
dayCounter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// src/components/datePicker/dom/buildDatePickerFooter.ts
|
||||||
|
import { formatDateForDisplay } from "../datePickerUtils"; // Use consistent display formatting
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the footer section of the date picker modal (Selected Date Display, Confirm Button).
|
||||||
|
* @param selectedDateObj - The currently selected date (Date object).
|
||||||
|
* @param onConfirm - Callback function to execute when the 'Select' button is clicked.
|
||||||
|
* @returns The HTMLElement representing the footer.
|
||||||
|
*/
|
||||||
|
export function buildDatePickerFooter(
|
||||||
|
selectedDateObj: Date,
|
||||||
|
onConfirm: () => void
|
||||||
|
): HTMLElement {
|
||||||
|
const footer = document.createElement("div");
|
||||||
|
footer.className = "calendar-footer";
|
||||||
|
|
||||||
|
// --- Display Currently Selected Date ---
|
||||||
|
const selectedDateDisplay = footer.createEl("span", { cls: "selected-date-display" });
|
||||||
|
selectedDateDisplay.textContent = `Selected: ${formatDateForDisplay(selectedDateObj)}`;
|
||||||
|
|
||||||
|
// --- Confirm Button ---
|
||||||
|
const confirmBtn = footer.createEl("button", { text: "Select", cls: "mod-cta" }); // Use Obsidian's primary button style
|
||||||
|
confirmBtn.onclick = onConfirm; // Assign the provided handler
|
||||||
|
|
||||||
|
return footer;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the header section of the date picker modal (Prev, Title, Next).
|
||||||
|
* @param currentDisplayMonth - The Date object representing the month/year being displayed.
|
||||||
|
* @param onPrevMonth - Callback function to execute when the 'Previous' button is clicked.
|
||||||
|
* @param onNextMonth - Callback function to execute when the 'Next' button is clicked.
|
||||||
|
* @returns The HTMLElement representing the header.
|
||||||
|
*/
|
||||||
|
export function buildDatePickerHeader(
|
||||||
|
currentDisplayMonth: Date,
|
||||||
|
onPrevMonth: () => void,
|
||||||
|
onNextMonth: () => void
|
||||||
|
): HTMLElement {
|
||||||
|
const header = document.createElement("div");
|
||||||
|
header.className = "calendar-header";
|
||||||
|
|
||||||
|
// --- Previous Month Button ---
|
||||||
|
const prevBtn = header.createEl("button", { text: "←", cls: "clickable-icon" });
|
||||||
|
prevBtn.setAttribute("aria-label", "Previous month");
|
||||||
|
prevBtn.onclick = onPrevMonth; // Assign the provided handler
|
||||||
|
|
||||||
|
// --- Month/Year Title ---
|
||||||
|
const title = header.createEl("span", {
|
||||||
|
cls: "calendar-title",
|
||||||
|
text: currentDisplayMonth.toLocaleString("default", { // Use user's default locale
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
timeZone: 'UTC' // Ensure month/year is based on UTC date
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Next Month Button ---
|
||||||
|
const nextBtn = header.createEl("button", { text: "→", cls: "clickable-icon" });
|
||||||
|
nextBtn.setAttribute("aria-label", "Next month");
|
||||||
|
nextBtn.onclick = onNextMonth; // Assign the provided handler
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Builds the row displaying weekday abbreviations (Mo, Tu, etc.).
|
||||||
|
* @param includeWeekNumber - If true, adds a "W#" header cell at the beginning.
|
||||||
|
* @returns The HTMLElement representing the weekday header row.
|
||||||
|
*/
|
||||||
|
export function buildWeekdayHeaders(includeWeekNumber: boolean = false): HTMLElement {
|
||||||
|
const weekdaysContainer = document.createElement("div");
|
||||||
|
weekdaysContainer.className = "calendar-weekdays";
|
||||||
|
|
||||||
|
//? Header for Week# column (optional)
|
||||||
|
if (includeWeekNumber) {
|
||||||
|
weekdaysContainer.createDiv({ cls: "week-number-header", text: "W#" });
|
||||||
|
}
|
||||||
|
|
||||||
|
//? Standard Monday-first weekdays
|
||||||
|
const weekdays = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
||||||
|
weekdays.forEach(day => weekdaysContainer.createDiv({ cls: "calendar-weekday", text: day }));
|
||||||
|
|
||||||
|
return weekdaysContainer;
|
||||||
|
}
|
||||||
43
src/codeblocks/dateHeader/index.ts
Normal file
43
src/codeblocks/dateHeader/index.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { MarkdownRenderChild, App } from 'obsidian';
|
||||||
|
|
||||||
|
import { DateNavigator } from './components/dateNavigator/DateNavigator';
|
||||||
|
import { pluginState } from '../../pluginState';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Obsidian Render Child responsible for rendering and managing the lifecycle
|
||||||
|
* of a DateNavigator instance within a Markdown code block.
|
||||||
|
*/
|
||||||
|
export class DateNavigatorRenderer extends MarkdownRenderChild {
|
||||||
|
|
||||||
|
private navigator: DateNavigator | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
containerEl: HTMLElement,
|
||||||
|
private appInstance: App
|
||||||
|
) {
|
||||||
|
super(containerEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by Obsidian when the component should be loaded/rendered. */
|
||||||
|
onload() {
|
||||||
|
this.containerEl.empty();
|
||||||
|
this.containerEl.addClass('date-navigator-container-wrapper');
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.navigator = new DateNavigator({
|
||||||
|
app: this.appInstance,
|
||||||
|
pluginState: pluginState,
|
||||||
|
containerEl: this.containerEl
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[DateNavigatorRenderer] Failed to create DateNavigator:", error);
|
||||||
|
this.containerEl.setText('Error loading date navigator component.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by Obsidian when the component should be unloaded/cleaned up. */
|
||||||
|
onunload() {
|
||||||
|
this.navigator?.destroy();
|
||||||
|
this.navigator = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
import { Modal, App, Notice, MarkdownView } from "obsidian";
|
|
||||||
import { pluginState } from "../../pluginState"; // Assuming this exists and works
|
|
||||||
|
|
||||||
function getISOWeekNumber(date: Date): number {
|
|
||||||
const temp = new Date(date.getTime());
|
|
||||||
temp.setHours(0, 0, 0, 0);
|
|
||||||
// Thursday in current week decides the year.
|
|
||||||
temp.setDate(temp.getDate() + 3 - ((temp.getDay() + 6) % 7));
|
|
||||||
const week1 = new Date(temp.getFullYear(), 0, 4);
|
|
||||||
return (
|
|
||||||
1 +
|
|
||||||
Math.round(
|
|
||||||
((temp.getTime() - week1.getTime()) / 86400000 - ((week1.getDay() + 6) % 7)) /
|
|
||||||
7
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DatePickerModal extends Modal {
|
|
||||||
private selectedDate: Date;
|
|
||||||
private currentMonth: Date;
|
|
||||||
private today: Date = new Date();
|
|
||||||
|
|
||||||
constructor(app: App) {
|
|
||||||
super(app);
|
|
||||||
// Ensure selectedDate is always a Date object
|
|
||||||
const initialDate = pluginState.selectedDate ? new Date(pluginState.selectedDate) : new Date();
|
|
||||||
// Validate if the parsed date is valid, otherwise default to today
|
|
||||||
this.selectedDate = isNaN(initialDate.getTime()) ? new Date() : initialDate;
|
|
||||||
// Set time to 00:00:00 to compare dates easily
|
|
||||||
this.selectedDate.setHours(0, 0, 0, 0);
|
|
||||||
this.today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
this.currentMonth = new Date(this.selectedDate.getFullYear(), this.selectedDate.getMonth(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpen() {
|
|
||||||
// Add a class to the modal content for easier styling
|
|
||||||
this.contentEl.addClass("custom-datepicker-modal-content");
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
private render() {
|
|
||||||
const { contentEl } = this;
|
|
||||||
contentEl.empty(); // Clear previous content
|
|
||||||
|
|
||||||
// --- Header ---
|
|
||||||
const header = contentEl.createEl("div", { cls: "calendar-header" });
|
|
||||||
const prevBtn = header.createEl("button", { text: "←", cls: "clickable-icon" });
|
|
||||||
prevBtn.setAttribute("aria-label", "Previous month");
|
|
||||||
prevBtn.onclick = () => {
|
|
||||||
this.currentMonth.setMonth(this.currentMonth.getMonth() - 1);
|
|
||||||
this.render();
|
|
||||||
};
|
|
||||||
|
|
||||||
const title = header.createEl("span", {
|
|
||||||
cls: "calendar-title",
|
|
||||||
text: this.currentMonth.toLocaleString("default", { month: "long", year: "numeric" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextBtn = header.createEl("button", { text: "→", cls: "clickable-icon" });
|
|
||||||
nextBtn.setAttribute("aria-label", "Next month");
|
|
||||||
nextBtn.onclick = () => {
|
|
||||||
this.currentMonth.setMonth(this.currentMonth.getMonth() + 1);
|
|
||||||
this.render();
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Weekday Headers ---
|
|
||||||
const weekdaysContainer = contentEl.createDiv({ cls: "calendar-weekdays" });
|
|
||||||
const weekdays = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
|
||||||
weekdaysContainer.createDiv({ cls: "week-number-header", text: "W#" }); // Header for Week#
|
|
||||||
weekdays.forEach(day => weekdaysContainer.createDiv({ cls: "calendar-weekday", text: day }));
|
|
||||||
|
|
||||||
// --- Calendar Grid ---
|
|
||||||
// buildCalendar now returns the grid itself
|
|
||||||
const calendarGrid = this.buildCalendar();
|
|
||||||
contentEl.appendChild(calendarGrid);
|
|
||||||
|
|
||||||
// --- Footer ---
|
|
||||||
const footer = contentEl.createDiv({ cls: "calendar-footer" });
|
|
||||||
// Display currently selected date
|
|
||||||
const selectedDateDisplay = footer.createEl("span", { cls: "selected-date-display" });
|
|
||||||
this.updateSelectedDateDisplay(selectedDateDisplay); // Initial display
|
|
||||||
|
|
||||||
const confirmBtn = footer.createEl("button", { text: "Select", cls: "mod-cta" }); // Use Obsidian's primary button style
|
|
||||||
confirmBtn.onclick = () => {
|
|
||||||
const iso = `${this.selectedDate.getFullYear()}-${String(this.selectedDate.getMonth() + 1).padStart(2, '0')}-${String(this.selectedDate.getDate()).padStart(2, '0')}`;
|
|
||||||
pluginState.selectedDate = iso; // Update global state
|
|
||||||
new Notice(`Date set to ${iso}`);
|
|
||||||
// Optional: Force re-render if your plugin uses the date reactively in preview
|
|
||||||
this.app.workspace.getActiveViewOfType(MarkdownView)?.previewMode?.rerender(true);
|
|
||||||
this.close();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildCalendar(): HTMLElement {
|
|
||||||
const grid = document.createElement("div");
|
|
||||||
grid.className = "calendar-grid"; // This will be the actual grid container
|
|
||||||
|
|
||||||
const year = this.currentMonth.getFullYear();
|
|
||||||
const month = this.currentMonth.getMonth();
|
|
||||||
const firstDayOfMonth = new Date(year, month, 1);
|
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
||||||
|
|
||||||
// Calculate offset for Monday start (0=Mon, 6=Sun)
|
|
||||||
const startOffset = (firstDayOfMonth.getDay() + 6) % 7;
|
|
||||||
|
|
||||||
// --- Add Week Numbers and Empty Cells before the 1st ---
|
|
||||||
let day = 1 - startOffset; // Start day counter considering offset
|
|
||||||
let currentWeekNumber = -1;
|
|
||||||
|
|
||||||
while (day <= daysInMonth) {
|
|
||||||
// Calculate and add week number cell for the current row
|
|
||||||
const dateForWeekNum = new Date(year, month, day > 0 ? day : 1); // Use day 1 if current day is placeholder
|
|
||||||
const weekNumber = getISOWeekNumber(dateForWeekNum);
|
|
||||||
// Only add week number if it changed (start of a new row essentially)
|
|
||||||
if (weekNumber !== currentWeekNumber) {
|
|
||||||
const weekEl = document.createElement("div");
|
|
||||||
weekEl.className = "week-number";
|
|
||||||
weekEl.textContent = `${String(weekNumber).padStart(2, "0")}`;
|
|
||||||
grid.appendChild(weekEl);
|
|
||||||
currentWeekNumber = weekNumber;
|
|
||||||
} else if (grid.children.length % 8 === 0) {
|
|
||||||
// If week number didn't change but we are at start of grid row, add it
|
|
||||||
// This handles month starts that continue week number from previous month correctly
|
|
||||||
const weekEl = document.createElement("div");
|
|
||||||
weekEl.className = "week-number";
|
|
||||||
weekEl.textContent = `${String(weekNumber).padStart(2, "0")}`;
|
|
||||||
grid.appendChild(weekEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Add the 7 day cells for the week
|
|
||||||
for (let i = 0; i < 7; i++) {
|
|
||||||
const cell = document.createElement("div"); // Use a div as container for button
|
|
||||||
cell.className = "calendar-day-cell";
|
|
||||||
|
|
||||||
if (day < 1 || day > daysInMonth) {
|
|
||||||
// Empty cell (before start or after end of month)
|
|
||||||
cell.classList.add("empty");
|
|
||||||
} else {
|
|
||||||
const currentDate = new Date(year, month, day);
|
|
||||||
currentDate.setHours(0, 0, 0, 0); // Normalize time for comparison
|
|
||||||
|
|
||||||
const btn = document.createElement("button");
|
|
||||||
btn.textContent = String(day);
|
|
||||||
btn.className = "calendar-day";
|
|
||||||
btn.setAttribute("aria-label", currentDate.toDateString());
|
|
||||||
|
|
||||||
// Check if this is the currently selected date
|
|
||||||
if (currentDate.getTime() === this.selectedDate.getTime()) {
|
|
||||||
btn.classList.add("selected");
|
|
||||||
btn.setAttribute("aria-selected", "true");
|
|
||||||
} else {
|
|
||||||
btn.setAttribute("aria-selected", "false");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is today's date
|
|
||||||
if (currentDate.getTime() === this.today.getTime()) {
|
|
||||||
btn.classList.add("today");
|
|
||||||
}
|
|
||||||
|
|
||||||
btn.onclick = () => {
|
|
||||||
// Update selected date state
|
|
||||||
this.selectedDate = currentDate;
|
|
||||||
|
|
||||||
const previouslySelected = grid.querySelector(".calendar-day.selected");
|
|
||||||
if (previouslySelected) {
|
|
||||||
previouslySelected.classList.remove("selected");
|
|
||||||
previouslySelected.setAttribute("aria-selected", "false");
|
|
||||||
}
|
|
||||||
btn.classList.add("selected");
|
|
||||||
btn.setAttribute("aria-selected", "true");
|
|
||||||
|
|
||||||
this.updateSelectedDateDisplay();
|
|
||||||
};
|
|
||||||
cell.appendChild(btn);
|
|
||||||
}
|
|
||||||
grid.appendChild(cell);
|
|
||||||
day++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateSelectedDateDisplay(element?: HTMLElement) {
|
|
||||||
const displayEl = element ?? this.contentEl.querySelector(".selected-date-display");
|
|
||||||
if (displayEl) {
|
|
||||||
displayEl.textContent = `Selected: ${this.selectedDate.toLocaleDateString()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
this.contentEl.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { App } from "obsidian";
|
|
||||||
import { DatePickerModal } from "./DatePickerModal";
|
|
||||||
|
|
||||||
export function renderDatePicker(container: HTMLElement, app: App) {
|
|
||||||
const wrapper = container.createDiv({ cls: "datepicker" });
|
|
||||||
|
|
||||||
const button = wrapper.createEl("button", { text: "Select Date" });
|
|
||||||
button.onclick = () => {
|
|
||||||
new DatePickerModal(app).open();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { processSqlBlock } from "./processSqlBlock";
|
import { processSqlBlock } from "./processSqlBlock";
|
||||||
import { processSqlChartBlock } from "./processSqlChartBlock";
|
import { processSqlChartBlock } from "./processSqlChartBlock";
|
||||||
import { renderDatePicker } from "./datePicker";
|
import { DateNavigatorRenderer } from "./dateHeader";
|
||||||
|
|
||||||
export { processSqlBlock, processSqlChartBlock, renderDatePicker };
|
export { processSqlBlock, processSqlChartBlock, DateNavigatorRenderer };
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
|
export const DATE_CHANGED_EVENT_NAME = 'plugin-date-changed';
|
||||||
|
|
||||||
export class PluginState {
|
export class PluginState {
|
||||||
private _selectedDate: string;
|
private _selectedDate: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._selectedDate = new Date().toISOString().split("T")[0];
|
this._selectedDate = new Date().toISOString().split("T")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedDate(): string {
|
get selectedDate(): string {
|
||||||
return this._selectedDate;
|
return this._selectedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
set selectedDate(newDate: string) {
|
set selectedDate(newIsoDate: string) {
|
||||||
this._selectedDate = newDate;
|
if (this._selectedDate !== newIsoDate) {
|
||||||
}
|
this._selectedDate = newIsoDate;
|
||||||
|
document.dispatchEvent(new CustomEvent(DATE_CHANGED_EVENT_NAME, {
|
||||||
|
detail: { newIsoDate: this._selectedDate } // Pass the new date
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
//& console.log(`[PluginState] Date unchanged: ${newIsoDate}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pluginState = new PluginState();
|
export const pluginState = new PluginState();
|
||||||
59
src/styles/dateNavigationInject.ts
Normal file
59
src/styles/dateNavigationInject.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
//? Define the unique class used by the DateNavigatorRenderer container
|
||||||
|
//? Define a unique ID for the style tag specific to the navigator
|
||||||
|
const STYLE_ID = "sqlitedb-date-navigator-styles";
|
||||||
|
const NAVIGATOR_CONTAINER_WRAPPER_CLASS = "date-navigator-container-wrapper";
|
||||||
|
|
||||||
|
/** Returns the CSS string specifically for the Date Navigator component. */
|
||||||
|
function getDateNavigatorStyles(): string {
|
||||||
|
return `
|
||||||
|
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} {
|
||||||
|
padding-bottom: var(--size-4-2);
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-navigator-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Stack nav row and button vertically */
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-4-2); /* Space between nav row and button */
|
||||||
|
}
|
||||||
|
|
||||||
|
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-nav-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--size-4-3);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} h1.date-navigator-display {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: var(--font-heading-1);
|
||||||
|
color: var(--text-error);
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 15ch;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Injects CSS for the Date Navigator component into the document head if not already present. */
|
||||||
|
export function injectDateNavigatorStyles(): void {
|
||||||
|
if (document.getElementById(STYLE_ID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleEl = document.createElement('style');
|
||||||
|
styleEl.id = STYLE_ID;
|
||||||
|
styleEl.textContent = getDateNavigatorStyles();
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the injected Date Navigator styles from the document head. */
|
||||||
|
export function removeDateNavigatorStyles(): void {
|
||||||
|
const styleEl = document.getElementById(STYLE_ID);
|
||||||
|
if (styleEl) {
|
||||||
|
styleEl.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
clearComponentErrorState,
|
clearComponentErrorState,
|
||||||
HabitCounterUIElements
|
HabitCounterUIElements
|
||||||
} from "./dom/uiUpdaters";
|
} from "./dom/uiUpdaters";
|
||||||
|
import { DATE_CHANGED_EVENT_NAME, pluginState } from "../../pluginState";
|
||||||
|
|
||||||
//? Main Web Component class, acts as an orchestrator.
|
//? Main Web Component class, acts as an orchestrator.
|
||||||
export class HabitCounter extends HTMLElement {
|
export class HabitCounter extends HTMLElement {
|
||||||
|
|
@ -36,6 +37,8 @@ export class HabitCounter extends HTMLElement {
|
||||||
wrapper: null, valueDisplay: null, labelElement: null, minusButton: null, plusButton: null
|
wrapper: null, valueDisplay: null, labelElement: null, minusButton: null, plusButton: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _isListeningForDateChanges: boolean = false; // Prevent multiple listeners
|
||||||
|
|
||||||
// --- Lifecycle ---
|
// --- Lifecycle ---
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if (this._isInitialized) return;
|
if (this._isInitialized) return;
|
||||||
|
|
@ -48,9 +51,15 @@ export class HabitCounter extends HTMLElement {
|
||||||
|
|
||||||
this.uiElements = buildHabitCounterDOM(this.attachShadow({ mode: "open" }), handlers); //~ Build DOM via helper
|
this.uiElements = buildHabitCounterDOM(this.attachShadow({ mode: "open" }), handlers); //~ Build DOM via helper
|
||||||
applyHabitCounterStyles(this.shadowRoot!); //~ Apply styles via helper
|
applyHabitCounterStyles(this.shadowRoot!); //~ Apply styles via helper
|
||||||
|
this._setupDateChangeListener(); //^ Call setup listener
|
||||||
this._isInitialized = true;
|
this._isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
document.removeEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
|
||||||
|
this._isListeningForDateChanges = false;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Public Methods ---
|
// --- Public Methods ---
|
||||||
public setDbService(service: DBService): void {
|
public setDbService(service: DBService): void {
|
||||||
if (!service) {
|
if (!service) {
|
||||||
|
|
@ -98,6 +107,32 @@ export class HabitCounter extends HTMLElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _setupDateChangeListener(): void {
|
||||||
|
//? Only add listener if component uses dynamic date and not already listening
|
||||||
|
if (this.getAttribute("date") === "@date" && !this._isListeningForDateChanges) {
|
||||||
|
document.addEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
|
||||||
|
this._isListeningForDateChanges = true;
|
||||||
|
} else {
|
||||||
|
//& console.log(`[HabitCounter Component ${this.habitKey}] Not adding listener (date=${this.getAttribute("date")}, listening=${this._isListeningForDateChanges})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleGlobalDateChange = (event: Event): void => {
|
||||||
|
//? Type guard for CustomEvent
|
||||||
|
if (!(event instanceof CustomEvent)) return;
|
||||||
|
|
||||||
|
const newIsoDate = event.detail?.newIsoDate;
|
||||||
|
|
||||||
|
//? Double-check this component should react (uses @date)
|
||||||
|
if (this.initialDate === "@date") {
|
||||||
|
|
||||||
|
//? Reload data using the same helper
|
||||||
|
loadHabitData(this).catch(err => {
|
||||||
|
console.error(`[HabitCounter Component ${this.habitKey}] Error reloading data after date change:`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
//? Public method called by click handlers via instance reference
|
//? Public method called by click handlers via instance reference
|
||||||
public async _updateData(delta: number): Promise<void> {
|
public async _updateData(delta: number): Promise<void> {
|
||||||
await updateHabitData(this, delta); //~ Update data via helper
|
await updateHabitData(this, delta); //~ Update data via helper
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue