This commit is contained in:
stfrigerio 2025-04-07 21:06:51 +02:00
parent d8cc77f863
commit 6550a6570e
3 changed files with 147 additions and 85 deletions

View file

@ -13,27 +13,47 @@ function getDateNavigatorStyles(): string {
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-navigator-wrapper { .${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-navigator-wrapper {
display: flex; display: flex;
flex-direction: column; /* Stack nav row and button vertically */ flex-direction: column;
align-items: center; align-items: center;
gap: var(--size-4-2); /* Space between nav row and button */ gap: var(--size-4-2);
} }
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-nav-row { .${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-nav-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: var(--size-4-3); gap: 2rem;
width: 100%; width: 100%;
} }
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-nav-button {
font-size: 2.5rem;
width: 3rem;
height: 3rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--text-normal);
transition: color 0.2s ease, transform 0.2s ease;
}
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-nav-button:hover {
color: var(--text-accent);
transform: scale(1.2);
}
.${NAVIGATOR_CONTAINER_WRAPPER_CLASS} h1.date-navigator-display { .${NAVIGATOR_CONTAINER_WRAPPER_CLASS} h1.date-navigator-display {
margin: 0; margin: 0;
text-align: center; text-align: center;
flex-grow: 1;
font-size: var(--font-heading-1); font-size: var(--font-heading-1);
color: var(--text-error); color: var(--text-normal);
white-space: nowrap; white-space: nowrap;
min-width: 15ch; min-width: 15ch;
padding: 0 1rem;
} }
`; `;
} }

View file

@ -8,7 +8,8 @@ import { createModalTriggerHandler } from "./eventHandlers/modalTriggerHandlers"
import { TextInputConfig, ModalType, TextInputEventProps, TextInputDOMElements } from "./TextInput.types"; import { TextInputConfig, ModalType, TextInputEventProps, TextInputDOMElements } from "./TextInput.types";
import { loadTextValue } from "./data/loadTextValue"; import { loadTextValue } from "./data/loadTextValue";
import { upsertTextValue } from "./data/upsertTextValue"; import { upsertTextValue } from "./data/upsertTextValue";
import { updateInputValue, setTextErrorState, clearTextErrorState } from "./dom/textInputUiUpdaters"; import { updateInputValue, setTextErrorState, clearTextErrorState, updateStaticTextUI } from "./dom/textInputUiUpdaters";
import { DATE_CHANGED_EVENT_NAME } from "src/pluginState";
export class TextInput extends HTMLElement { export class TextInput extends HTMLElement {
// --- Dependencies --- // --- Dependencies ---
@ -25,95 +26,98 @@ export class TextInput extends HTMLElement {
public valueCol: string = ""; public valueCol: string = "";
public dateCol: string = ""; public dateCol: string = "";
private _isListeningForDateChanges: boolean = false; // Prevent multiple listeners
// --- Shadow DOM Elements --- // --- Shadow DOM Elements ---
private uiElements!: TextInputDOMElements; private uiElements!: TextInputDOMElements;
// --- Lifecycle --- // --- Lifecycle ---
connectedCallback() { connectedCallback() {
if (this._isInitialized) return; if (this._isInitialized) return;
// Initialization requires appInstance
//~ Initialization logic will be triggered by setDependencies,
//~ but we need to build the DOM structure and setup listeners that
//~ depend only on attributes right away.
//~ Create bound event handler (needed by DOM builder if input acts as trigger)
const eventProps: TextInputEventProps = { // Partial props okay here? Or defer fully? Let's defer.
app: this.appInstance!, // Assume app will be set, riskier
uiElements: this.uiElements, // Placeholder
currentValue: this.currentValue,
setValue: this._setValue,
// Config will be filled later
};
const handlers = {
handleInputChange: createInputChangeHandler(eventProps), // Needs props later
handleModalTrigger: createModalTriggerHandler(eventProps), // Needs props later
};
//? Read config attributes needed *immediately* for DOM build (like isButton, modalType)
const initialConfig: TextInputConfig = {
modalType: (this.getAttribute("data-modal-type") as ModalType) ?? 'none',
isButton: this.getAttribute("data-is-button")?.toLowerCase() === 'true',
// Read others needed by buildTextInputDOM, possibly placeholder/initialValueAttr too
placeholder: this.getAttribute("placeholder") ?? undefined,
initialValueAttr: this.getAttribute("data-initial-value") ?? "",
label: this.getAttribute("data-label") ?? undefined,
};
//? Build DOM structure immediately
this.uiElements = buildTextInputDOM(this.attachShadow({ mode: "open" }), initialConfig, handlers);
applyTextInputStyles(this.shadowRoot!);
//? This reads the attribute directly, safe to call now.
this._setupDateChangeListener();
this._isInitialized = true; // Mark basic init done
}
disconnectedCallback() {
if (this._isListeningForDateChanges) {
document.removeEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
this._isListeningForDateChanges = false;
}
} }
// --- Public Methods --- // --- Public Methods ---
/** Injects DBService first, then App instance. */ /** Injects DBService first, then App instance. */
public setDependencies(dbService: DBService, app: App): void { public setDependencies(dbService: DBService, app: App): void {
if (!dbService || !app) { if (!dbService || !app) { /* ... error ... */ return; }
console.error("[TextInput] Invalid DBService or App instance provided.");
//? Maybe show an error state on the element directly?
this.textContent = "[Init Error]";
return;
}
this.appInstance = app; this.appInstance = app;
try { try {
this.textDataService = new TextInputDataService(dbService); //^ Init Data Service this.textDataService = new TextInputDataService(dbService);
} catch(error){ } catch (error) { /* ... error ... */ return; }
console.error("[TextInput] Failed to initialize TextInputDataService:", error);
this.textContent = "[Data Service Error]";
return;
}
if (!this._isInitialized) { //? Now that dependencies are set, complete the setup
this._readAttributesAndBuildConfig(); this._readAttributesAndInitializeData();
this._initializeComponent();
this._isInitialized = true;
}
} }
// --- Internal Initialization --- /** Reads data-related attributes and triggers initial load. Called AFTER dependencies are set. */
private _readAttributesAndBuildConfig(): void { private _readAttributesAndInitializeData(): void {
this.config = { // --- Read Data Attributes ---
// UI Config this.table = this.getAttribute("data-table") ?? "";
label: this.getAttribute("data-label") ?? undefined, this.date = this.getAttribute("data-date") ?? "@date";
placeholder: this.getAttribute("placeholder") ?? undefined, this.valueCol = this.getAttribute("data-value-col") ?? "";
initialValueAttr: this.getAttribute("data-initial-value") ?? "", // Read initial value from attr this.dateCol = this.getAttribute("data-date-col") ?? "";
modalType: (this.getAttribute("data-modal-type") as ModalType) ?? 'none',
isButton: this.getAttribute("data-is-button")?.toLowerCase() === 'true', //^ Read isButton flag
// Data Config // --- Update Config object
table: this.getAttribute("data-table") ?? undefined, this.config.table = this.table;
date: this.getAttribute("data-date") ?? "@date", // Default to @date this.config.date = this.date;
valueCol: this.getAttribute("data-value-col") ?? undefined, this.config.valueCol = this.valueCol;
dateCol: this.getAttribute("data-date-col") ?? undefined, this.config.dateCol = this.dateCol;
};
//? Copy needed values from config/attributes to 'this' properties
this.table = this.config.table ?? "";
this.date = this.config.date ?? "@date";
this.valueCol = this.config.valueCol ?? "";
this.dateCol = this.config.dateCol ?? "";
}
private _initializeComponent(): void { // --- Trigger Initial Load ---
if (!this.appInstance) { requestAnimationFrame(() => {
console.error("[TextInput] Cannot initialize: App instance not set."); updateStaticTextUI(this.uiElements, this.config.label);
return; if (!this.table || !this.valueCol || !this.dateCol) {
} console.warn(`[TextInput ${this.config.label}] Missing required data configuration attributes (table, valueCol, dateCol).`);
this.showErrorState("Config Error"); this._disableInput?.();
const eventProps: TextInputEventProps = { }
...this.config, // Includes data config now loadTextValue(this).catch(err => console.error(`[TextInput ${this.config.label}] Unhandled error during initial load:`, err));
app: this.appInstance, });
uiElements: this.uiElements, // Placeholder
currentValue: this.currentValue,
setValue: this._setValue,
};
const handlers = {
handleInputChange: createInputChangeHandler(eventProps),
handleModalTrigger: createModalTriggerHandler(eventProps),
};
this.uiElements = buildTextInputDOM(this.attachShadow({ mode: "open" }), this.config, handlers);
eventProps.uiElements = this.uiElements;
applyTextInputStyles(this.shadowRoot!);
//^ --- Trigger Initial Load (if data configured) ---
if (this.config.table && this.config.date && this.config.valueCol && this.config.dateCol) {
loadTextValue(this).catch(err => console.error(`[TextInput ${this.config.table}] Unhandled error during initial load:`, err));
} else {
//? No data config, just use initialValueAttr
this._updateDisplay(this.config.initialValueAttr ?? "");
}
// todo: date change listener if needed
} }
/** Central method to update state, UI, and trigger save. */ /** Central method to update state, UI, and trigger save. */
@ -138,6 +142,34 @@ export class TextInput extends HTMLElement {
} }
}; };
private _setupDateChangeListener(): void {
const dateAttr = this.getAttribute("data-date"); // Read attribute directly
if (dateAttr === "@date" && !this._isListeningForDateChanges) {
document.addEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
this._isListeningForDateChanges = true;
}
}
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.date === "@date") {
//& console.log(`[TextInput ${this.uniqueId}] Date property matches "@date", reloading data...`);
this.classList.add('loading');
this._updateDisplay("..."); // Show loading dots
loadTextValue(this)
.catch(err => { /* ... */ })
.finally(() => { this.classList.remove('loading'); });
} else {
//& console.log(`[TextInput ${this.uniqueId}] Ignoring date change event (internal date property is "${this.date}").`);
}
};
//? Public method called by data helpers to update display //? Public method called by data helpers to update display
public _updateDisplay(newValue: string): void { public _updateDisplay(newValue: string): void {
this.currentValue = newValue; // Update internal state this.currentValue = newValue; // Update internal state
@ -149,8 +181,9 @@ export class TextInput extends HTMLElement {
//? Public methods for UI state helpers //? Public methods for UI state helpers
public showErrorState(message?: string): void { setTextErrorState(this, this.uiElements, message); } public showErrorState(message?: string): void { setTextErrorState(this, this.uiElements, message); }
public clearErrorState(): void { clearTextErrorState(this, this.uiElements); } public clearErrorState(): void { clearTextErrorState(this, this.uiElements); }
// todo: disable/enable methods private _enableInput(): void { if(this.uiElements?.inputElement) this.uiElements.inputElement.disabled = false; this.removeAttribute('aria-disabled'); }
private _disableInput(): void { if(this.uiElements?.inputElement) this.uiElements.inputElement.disabled = true; this.setAttribute('aria-disabled', 'true'); }
} }
// --- Custom Element Definition --- // --- Custom Element Definition ---

View file

@ -1,8 +1,12 @@
import { DBService } from "src/DBService"; import { DBService } from "src/DBService";
import { TextRecord, TextDataArgs, UpsertTextArgs } from "../TextInput/TextInput.types"; import { TextDataArgs, UpsertTextArgs } from "../TextInput/TextInput.types";
import { quoteSqlIdentifier } from "./utils/quoteSqlIdentifier"; import { quoteSqlIdentifier } from "./utils/quoteSqlIdentifier";
import { validateBaseTextArgs, validateUpsertTextArgs } from "./utils/validateTextInputArgs"; import { validateBaseTextArgs, validateUpsertTextArgs } from "./utils/validateTextInputArgs";
interface RawTextRecord {
value: string | number | null;
}
export class TextInputDataService { export class TextInputDataService {
private dbService: DBService; private dbService: DBService;
constructor(dbService: DBService) { constructor(dbService: DBService) {
@ -10,7 +14,7 @@ export class TextInputDataService {
this.dbService = dbService; this.dbService = dbService;
} }
/** //? Fetches the current text value for a specific key/date. Returns empty string if not found. */ /** Fetches the current text value for a specific key/date. Returns empty string if not found. */
async fetchTextValue(args: TextDataArgs): Promise<string> { async fetchTextValue(args: TextDataArgs): Promise<string> {
try { try {
validateBaseTextArgs(args); validateBaseTextArgs(args);
@ -21,10 +25,15 @@ export class TextInputDataService {
const sql = `SELECT ${safeValueCol} AS value FROM ${safeTable} WHERE ${safeDateCol} = ?`; const sql = `SELECT ${safeValueCol} AS value FROM ${safeTable} WHERE ${safeDateCol} = ?`;
const params = [args.date]; const params = [args.date];
const result = await this.dbService.getQuery<TextRecord>(sql, params); const result = await this.dbService.getQuery<RawTextRecord>(sql, params);
const value = result?.[0]?.value; const value = result?.[0]?.value;
//? Return fetched string or empty string if null/undefined/not found
return typeof value === 'string' ? value : ""; if (value !== null && value !== undefined) {
const stringValue = String(value); // Convert number or string using String()
return stringValue;
} else {
return ""; // Return empty string for null/undefined/not found
}
} catch (error) { } catch (error) {
console.error(`[TextInputDataService] Error in fetchTextValue for ${args.table}:`, error); console.error(`[TextInputDataService] Error in fetchTextValue for ${args.table}:`, error);
@ -32,7 +41,7 @@ export class TextInputDataService {
} }
} }
/** //? Upserts the text value. Handles optional UUID column logic. */ /** Upserts the text value. Handles optional UUID column logic. */
async upsertTextValue(args: UpsertTextArgs): Promise<void> { async upsertTextValue(args: UpsertTextArgs): Promise<void> {
try { try {
validateUpsertTextArgs(args); validateUpsertTextArgs(args);