mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
pfft
This commit is contained in:
parent
d8cc77f863
commit
6550a6570e
3 changed files with 147 additions and 85 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ---
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue