date selector modal and date parsing in blocks

This commit is contained in:
stfrigerio 2025-04-04 18:09:53 +02:00
parent a3be482a27
commit f97f55ff67
8 changed files with 414 additions and 10 deletions

21
main.ts
View file

@ -3,7 +3,6 @@ import {
Plugin,
PluginSettingTab,
Setting,
Notice,
FileSystemAdapter,
Editor,
MarkdownView,
@ -12,22 +11,23 @@ import {
import { DBService } from "./src/dbService";
import { inspectTableStructure, convertEntriesInNotes } from "./src/commands";
import { processSqlBlock, processSqlChartBlock } from "./src/codeblocks";
import { processSqlBlock, processSqlChartBlock, renderDatePicker } from "./src/codeblocks";
import { pickTableName } from "./src/helpers";
import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types";
import { injectDatePickerStyles } from "src/styles/datePickerInject";
export default class SQLiteDBPlugin extends Plugin {
settings: SQLiteDBSettings;
private dbService: DBService;
async onload() {
console.log("Loading SQLiteDBPlugin...");
// init
await this.loadSettings();
this.dbService = new DBService(this.app);
await this.openDatabase();
injectDatePickerStyles();
this.addCommand({
id: "inspect-table-structure",
name: "Inspect table structure",
@ -42,7 +42,7 @@ export default class SQLiteDBPlugin extends Plugin {
id: "dump-table-to-notes",
name: "Dump table to notes",
callback: async () => {
await this.openDatabase(); // ensure DB is loaded
await this.openDatabase();
// 1) pick a table
const chosenTable = await pickTableName(this.dbService, this.app);
@ -56,7 +56,7 @@ export default class SQLiteDBPlugin extends Plugin {
});
this.registerMarkdownCodeBlockProcessor(
"sql", // <-- the name of your code block (```sql)
"sql",
async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
await processSqlBlock(this.dbService, source, el);
}
@ -69,6 +69,13 @@ export default class SQLiteDBPlugin extends Plugin {
}
);
this.registerMarkdownCodeBlockProcessor(
"date-picker",
async (source: string, el: HTMLElement) => {
renderDatePicker(el, this.app);
}
);
this.addSettingTab(new SQLiteDBSettingTab(this.app, this));
}

View file

@ -0,0 +1,197 @@
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.toISOString().split("T")[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();
}
}

View file

@ -0,0 +1,11 @@
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();
};
}

View file

@ -1,4 +1,5 @@
import { processSqlBlock } from "./processSqlBlock";
import { processSqlChartBlock } from "./processSqlChartBlock";
import { renderDatePicker } from "./datePicker";
export { processSqlBlock, processSqlChartBlock };
export { processSqlBlock, processSqlChartBlock, renderDatePicker };

View file

@ -1,6 +1,7 @@
import { Notice } from "obsidian";
import { DBService } from "../../dbService";
import { parseSqlParams, validateTable, buildSqlQuery, renderResults } from "./helpers";
import { pluginState } from "../../pluginState";
export async function processSqlBlock(dbService: DBService, source: string, el: HTMLElement) {
const db = dbService.getDB();
@ -10,7 +11,10 @@ export async function processSqlBlock(dbService: DBService, source: string, el:
return;
}
const params = parseSqlParams(source);
// replace @date with the selected date
const injectedSource = source.replace(/@date/g, pluginState.selectedDate);
const params = parseSqlParams(injectedSource);
if (!params) {
el.createEl("p", { text: "Missing required parameter: table" });
el.createEl("p", { text: "Example usage:" });

View file

@ -2,6 +2,7 @@ import { Notice } from "obsidian";
import { DBService } from "../../dbService";
import { parseChartParams, validateColumns, buildSqlQuery, processChartData } from "./helpers";
import { ChartType, createChart } from "./charts";
import { pluginState } from "../../pluginState";
export async function processSqlChartBlock(dbService: DBService, source: string, el: HTMLElement) {
const db = dbService.getDB();
@ -11,7 +12,10 @@ export async function processSqlChartBlock(dbService: DBService, source: string,
return;
}
const config = parseChartParams(source);
// replace @date with the selected date
const injectedSource = source.replace(/@date/g, pluginState.selectedDate);
const config = parseChartParams(injectedSource);
if (!config) {
// @ts-ignore
const message = config?.chartType === 'pie'

17
src/pluginState.ts Normal file
View file

@ -0,0 +1,17 @@
export class PluginState {
private _selectedDate: string;
constructor() {
this._selectedDate = new Date().toISOString().split("T")[0];
}
get selectedDate(): string {
return this._selectedDate;
}
set selectedDate(newDate: string) {
this._selectedDate = newDate;
}
}
export const pluginState = new PluginState();

View file

@ -0,0 +1,163 @@
export function injectDatePickerStyles() {
const styleId = "custom-datepicker-styles";
// Avoid injecting multiple times
if (document.getElementById(styleId)) {
return;
}
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
/* Style the modal content area */
.custom-datepicker-modal-content {
padding: var(--size-4-4) var(--size-4-6); /* Consistent padding */
}
/* Header: Title and Navigation Buttons */
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--size-4-3); /* Space below header */
}
.calendar-title {
font-weight: var(--font-bold);
font-size: var(--font-ui-large);
}
.calendar-header button.clickable-icon {
/* Use Obsidian's icon button style */
padding: var(--size-4-1) var(--size-4-2);
font-size: var(--font-ui-large); /* Make arrows bigger */
line-height: 1;
}
/* Weekday Headers Row */
.calendar-weekdays {
display: grid;
grid-template-columns: 36px repeat(7, 1fr); /* Week# + 7 days */
gap: var(--size-4-1);
padding-bottom: var(--size-4-1);
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: var(--size-4-2);
}
.calendar-weekday,
.week-number-header {
text-align: center;
font-weight: var(--font-semibold);
font-size: var(--font-ui-small);
color: var(--text-muted);
line-height: 1; /* Ensure consistent height */
}
.week-number-header {
font-style: italic;
}
/* Calendar Grid: Week Numbers and Day Cells */
.calendar-grid {
display: grid;
grid-template-columns: 36px repeat(7, 1fr); /* Week# + 7 days */
gap: var(--size-4-1); /* Gap between cells */
align-items: center; /* Vertically center content in grid rows */
}
/* Individual cell container (holds button or is empty) */
.calendar-day-cell {
display: flex; /* Center button inside */
justify-content: center;
align-items: center;
aspect-ratio: 1 / 1; /* Make cells squareish */
min-height: 30px; /* Ensure minimum height */
}
.calendar-day-cell.empty {
/* Style for empty cells (before/after month) */
/* background-color: var(--background-secondary); */ /* Subtle background */
/* opacity: 0.5; */
/* Or just leave them blank */
}
/* Week Number Cells in the main grid */
.week-number {
text-align: center;
font-size: var(--font-ui-smaller);
color: var(--text-faint);
font-style: italic;
align-self: center; /* Align vertically in grid row */
padding: 0 var(--size-4-1); /* Add some horizontal padding */
line-height: var(--line-height-normal);
}
/* Day Buttons */
.calendar-day {
display: flex;
align-items: center;
justify-content: center;
width: 100%; /* Fill cell width */
height: 100%; /* Fill cell height */
padding: var(--size-4-1);
border: 1px solid transparent; /* Reserve space for border */
border-radius: var(--radius-s);
background-color: var(--background-secondary);
color: var(--text-normal);
font-size: var(--font-ui-small);
cursor: pointer;
transition: all 0.1s ease-in-out;
line-height: 1; /* Prevent text pushing button size */
}
.calendar-day:hover {
background-color: var(--background-modifier-hover);
color: var(--text-normal); /* Keep text readable on hover */
border-color: var(--background-modifier-border-hover);
}
/* Today's Date Indicator */
.calendar-day.today {
border-color: var(--text-accent); /* Use accent color for border */
/* Optional: Add a subtle background or different font weight */
/* background-color: var(--background-primary-alt); */
/* font-weight: var(--font-bold); */
}
/* Selected Date Styling */
.calendar-day.selected {
background-color: var(--interactive-accent); /* Use theme's accent color */
color: var(--text-on-accent);
font-weight: var(--font-bold);
border-color: var(--interactive-accent-hover); /* Slightly darker border */
}
.calendar-day.selected:hover {
background-color: var(--interactive-accent-hover); /* Darken on hover */
color: var(--text-on-accent);
}
/* Today and Selected */
.calendar-day.today.selected {
/* Optional: Make combo distinct if needed, e.g., thicker border */
/* border-width: 2px; */
}
/* Footer Area */
.calendar-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--size-4-3); /* Space above footer */
padding-top: var(--size-4-2);
border-top: 1px solid var(--background-modifier-border);
}
.selected-date-display {
font-size: var(--font-ui-small);
color: var(--text-muted);
}
.calendar-footer button {
/* Let .mod-cta handle primary button style */
/* Add margin if needed */
/* margin-left: var(--size-4-2); */
}
`;
document.head.appendChild(style);
}