diff --git a/src/styles/dateNavigationInject.ts b/src/styles/dateNavigationInject.ts index 74041fc..6f31f7e 100644 --- a/src/styles/dateNavigationInject.ts +++ b/src/styles/dateNavigationInject.ts @@ -13,27 +13,47 @@ function getDateNavigatorStyles(): string { .${NAVIGATOR_CONTAINER_WRAPPER_CLASS} .date-navigator-wrapper { display: flex; - flex-direction: column; /* Stack nav row and button vertically */ + flex-direction: column; 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 { display: flex; align-items: center; justify-content: center; - gap: var(--size-4-3); + gap: 2rem; 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 { margin: 0; text-align: center; - flex-grow: 1; font-size: var(--font-heading-1); - color: var(--text-error); + color: var(--text-normal); white-space: nowrap; min-width: 15ch; + padding: 0 1rem; } `; } diff --git a/src/webcomponents/TextInput/TextInput.ts b/src/webcomponents/TextInput/TextInput.ts index 18c105e..ce4b623 100644 --- a/src/webcomponents/TextInput/TextInput.ts +++ b/src/webcomponents/TextInput/TextInput.ts @@ -8,7 +8,8 @@ import { createModalTriggerHandler } from "./eventHandlers/modalTriggerHandlers" import { TextInputConfig, ModalType, TextInputEventProps, TextInputDOMElements } from "./TextInput.types"; import { loadTextValue } from "./data/loadTextValue"; 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 { // --- Dependencies --- @@ -25,95 +26,98 @@ export class TextInput extends HTMLElement { public valueCol: string = ""; public dateCol: string = ""; + private _isListeningForDateChanges: boolean = false; // Prevent multiple listeners + + // --- Shadow DOM Elements --- private uiElements!: TextInputDOMElements; // --- Lifecycle --- connectedCallback() { 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 --- /** Injects DBService first, then App instance. */ public setDependencies(dbService: DBService, app: App): void { - if (!dbService || !app) { - console.error("[TextInput] Invalid DBService or App instance provided."); - //? Maybe show an error state on the element directly? - this.textContent = "[Init Error]"; - return; - } + if (!dbService || !app) { /* ... error ... */ return; } this.appInstance = app; try { - this.textDataService = new TextInputDataService(dbService); //^ Init Data Service - } catch(error){ - console.error("[TextInput] Failed to initialize TextInputDataService:", error); - this.textContent = "[Data Service Error]"; - return; - } + this.textDataService = new TextInputDataService(dbService); + } catch (error) { /* ... error ... */ return; } - if (!this._isInitialized) { - this._readAttributesAndBuildConfig(); - this._initializeComponent(); - this._isInitialized = true; - } + //? Now that dependencies are set, complete the setup + this._readAttributesAndInitializeData(); } - // --- Internal Initialization --- - private _readAttributesAndBuildConfig(): void { - this.config = { - // UI Config - label: this.getAttribute("data-label") ?? undefined, - placeholder: this.getAttribute("placeholder") ?? undefined, - initialValueAttr: this.getAttribute("data-initial-value") ?? "", // Read initial value from attr - modalType: (this.getAttribute("data-modal-type") as ModalType) ?? 'none', - isButton: this.getAttribute("data-is-button")?.toLowerCase() === 'true', //^ Read isButton flag + /** Reads data-related attributes and triggers initial load. Called AFTER dependencies are set. */ + private _readAttributesAndInitializeData(): void { + // --- Read Data Attributes --- + this.table = this.getAttribute("data-table") ?? ""; + this.date = this.getAttribute("data-date") ?? "@date"; + this.valueCol = this.getAttribute("data-value-col") ?? ""; + this.dateCol = this.getAttribute("data-date-col") ?? ""; - // Data Config - table: this.getAttribute("data-table") ?? undefined, - date: this.getAttribute("data-date") ?? "@date", // Default to @date - valueCol: this.getAttribute("data-value-col") ?? undefined, - dateCol: this.getAttribute("data-date-col") ?? undefined, - }; - //? 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 ?? ""; - } + // --- Update Config object + this.config.table = this.table; + this.config.date = this.date; + this.config.valueCol = this.valueCol; + this.config.dateCol = this.dateCol; - private _initializeComponent(): void { - if (!this.appInstance) { - console.error("[TextInput] Cannot initialize: App instance not set."); - return; - } - - const eventProps: TextInputEventProps = { - ...this.config, // Includes data config now - 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 + // --- Trigger Initial Load --- + requestAnimationFrame(() => { + updateStaticTextUI(this.uiElements, this.config.label); + 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?.(); + } + loadTextValue(this).catch(err => console.error(`[TextInput ${this.config.label}] Unhandled error during initial load:`, err)); + }); } /** 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 _updateDisplay(newValue: string): void { this.currentValue = newValue; // Update internal state @@ -149,8 +181,9 @@ export class TextInput extends HTMLElement { //? Public methods for UI state helpers public showErrorState(message?: string): void { setTextErrorState(this, this.uiElements, message); } 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 --- diff --git a/src/webcomponents/services/TextInputDataService.ts b/src/webcomponents/services/TextInputDataService.ts index 681bfd1..14f2c58 100644 --- a/src/webcomponents/services/TextInputDataService.ts +++ b/src/webcomponents/services/TextInputDataService.ts @@ -1,8 +1,12 @@ 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 { validateBaseTextArgs, validateUpsertTextArgs } from "./utils/validateTextInputArgs"; +interface RawTextRecord { + value: string | number | null; +} + export class TextInputDataService { private dbService: DBService; constructor(dbService: DBService) { @@ -10,7 +14,7 @@ export class TextInputDataService { 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 { try { validateBaseTextArgs(args); @@ -21,10 +25,15 @@ export class TextInputDataService { const sql = `SELECT ${safeValueCol} AS value FROM ${safeTable} WHERE ${safeDateCol} = ?`; const params = [args.date]; - const result = await this.dbService.getQuery(sql, params); + const result = await this.dbService.getQuery(sql, params); 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) { 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 { try { validateUpsertTextArgs(args);