mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
fixed button text input and time picker
This commit is contained in:
parent
6550a6570e
commit
48cb74f09a
20 changed files with 381 additions and 182 deletions
|
|
@ -1,6 +1,5 @@
|
|||
// src/components/dateNavigator/dom/buildDateNavigatorDOM.ts
|
||||
import { formatDateForDisplay, parseDateISO } from "../../datePicker/datePickerUtils";
|
||||
import type { DateNavigatorDOMElements } from "../dateNavigator.types"; // Type only
|
||||
import { formatDateForDisplay, parseDateISO } from "src/helpers/dateUtils";
|
||||
import type { DateNavigatorDOMElements } from "../dateNavigator.types";
|
||||
|
||||
//? Structure for the handlers needed by the builder
|
||||
export interface DateNavigatorHandlers {
|
||||
|
|
@ -15,7 +14,7 @@ export function buildDateNavigatorDOM(
|
|||
initialIsoDate: string,
|
||||
handlers: DateNavigatorHandlers
|
||||
): DateNavigatorDOMElements {
|
||||
containerEl.empty(); // Clear container first
|
||||
containerEl.empty();
|
||||
const wrapper = containerEl.createDiv({ cls: "date-navigator-wrapper" });
|
||||
const navRow = wrapper.createDiv({ cls: "date-nav-row" });
|
||||
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
// src/components/dateNavigator/eventHandlers/dateNavigationHandlers.ts
|
||||
import { App } from "obsidian";
|
||||
import { DatePickerModal } from "../../datePicker/DatePickerModal";
|
||||
import { DatePickerModal } from "src/components/DatePickerModal/DatePickerModal";
|
||||
import { PluginState } from "src/pluginState";
|
||||
import { parseDateISO, formatDateISO } from "../../datePicker/datePickerUtils";
|
||||
import { parseDateISO, formatDateISO } from "src/helpers/dateUtils";
|
||||
import { NavigationPeriod } from "../dateNavigator.types";
|
||||
|
||||
/** Updates the global plugin state with the new date and triggers necessary updates */
|
||||
|
|
@ -10,13 +9,10 @@ function updateGlobalDate(newDate: Date, pluginState: PluginState, app: App): vo
|
|||
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 */
|
||||
/** Creates the handler for the "previous" button click */
|
||||
export function createPrevHandler(
|
||||
pluginState: PluginState,
|
||||
period: NavigationPeriod,
|
||||
|
|
@ -49,7 +45,6 @@ export function createNextHandler(
|
|||
): () => 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);
|
||||
|
|
@ -72,8 +67,6 @@ export function createOpenModalHandler(
|
|||
): () => 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();
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { MarkdownRenderChild, App } from 'obsidian';
|
||||
|
||||
import { DateNavigator } from './components/dateNavigator/DateNavigator';
|
||||
import { DateNavigator } from './DateNavigator';
|
||||
import { pluginState } from '../../pluginState';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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";
|
||||
import { formatDateISO, parseDateISO } from "src/helpers/dateUtils";
|
||||
|
||||
export class DatePickerModal extends Modal {
|
||||
private selectedDateObj: Date; //? Internal state as Date object
|
||||
0
src/components/DatePickerModal/datePickerUtils.ts
Normal file
0
src/components/DatePickerModal/datePickerUtils.ts
Normal file
|
|
@ -1,4 +1,4 @@
|
|||
import { getISOWeekNumber } from "../datePickerUtils";
|
||||
import { getISOWeekNumber } from "src/helpers/dateUtils";
|
||||
|
||||
/**
|
||||
* Builds the main grid of days for the calendar month.
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
// src/components/datePicker/dom/buildDatePickerFooter.ts
|
||||
import { formatDateForDisplay } from "../datePickerUtils"; // Use consistent display formatting
|
||||
import { formatDateForDisplay } from "src/helpers/dateUtils";
|
||||
|
||||
/**
|
||||
* Builds the footer section of the date picker modal (Selected Date Display, Confirm Button).
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { App, Modal, Notice, Setting } from "obsidian";
|
||||
|
||||
//? A very basic example modal for selecting a time.
|
||||
export class TimePickerModal extends Modal {
|
||||
private currentTime: string; // e.g., "HH:MM"
|
||||
private onSelectCallback: (selectedTime: string) => void;
|
||||
|
||||
constructor(app: App, initialTime: string | undefined, onSelect: (selectedTime: string) => void) {
|
||||
super(app);
|
||||
//? Basic validation or default
|
||||
this.currentTime = initialTime && /^\d{2}:\d{2}$/.test(initialTime) ? initialTime : "12:00";
|
||||
this.onSelectCallback = onSelect;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("time-picker-modal-content"); // For styling
|
||||
|
||||
contentEl.createEl("h2", { text: "Select Time" });
|
||||
|
||||
// --- Basic Time Input (replace with better UI later) ---
|
||||
let tempTime = this.currentTime;
|
||||
new Setting(contentEl)
|
||||
.setName("Time (HH:MM)")
|
||||
.addText(text => text
|
||||
.setValue(tempTime)
|
||||
.setPlaceholder("HH:MM")
|
||||
.onChange(value => {
|
||||
// Basic format check
|
||||
if (/^\d{1,2}:\d{1,2}$/.test(value)) {
|
||||
// Format to HH:MM
|
||||
const parts = value.split(':');
|
||||
const hh = parts[0].padStart(2,'0');
|
||||
const mm = parts[1].padStart(2,'0');
|
||||
// Basic hour/minute range check (can be improved)
|
||||
if (parseInt(hh) >= 0 && parseInt(hh) <= 23 && parseInt(mm) >= 0 && parseInt(mm) <= 59) {
|
||||
tempTime = `${hh}:${mm}`;
|
||||
} else {
|
||||
// Handle invalid range slightly? Or let confirm check.
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// --- Action Buttons ---
|
||||
new Setting(contentEl)
|
||||
.addButton(button => button
|
||||
.setButtonText("Cancel")
|
||||
.onClick(() => this.close()))
|
||||
.addButton(button => button
|
||||
.setButtonText("Confirm")
|
||||
.setCta() // Make it the primary action
|
||||
.onClick(() => {
|
||||
//? Final validation before confirming
|
||||
if (/^\d{2}:\d{2}$/.test(tempTime)) {
|
||||
this.onSelectCallback(tempTime);
|
||||
this.close();
|
||||
} else {
|
||||
new Notice("Invalid time format. Use HH:MM.");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
64
src/components/TimePickerModal/TimePickerModal.ts
Normal file
64
src/components/TimePickerModal/TimePickerModal.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { App, Modal } from "obsidian";
|
||||
import { buildTimePickerDOM, buildTimePickerActions, TimePickerDOMElements } from "./dom/buildTimePickerDOM";
|
||||
import { parseTimeString, formatTimeString } from "./timePickerUtils";
|
||||
|
||||
export class TimePickerModal extends Modal {
|
||||
private initialHour: number;
|
||||
private initialMinute: number;
|
||||
private onSelectCallback: (selectedTime: string) => void;
|
||||
|
||||
//? References to the dropdown elements
|
||||
private uiElements: TimePickerDOMElements | null = null;
|
||||
|
||||
constructor(app: App, initialTime: string | undefined, onSelect: (selectedTime: string) => void) {
|
||||
super(app);
|
||||
this.onSelectCallback = onSelect;
|
||||
|
||||
//? Parse initial time into numbers
|
||||
const { hour, minute } = parseTimeString(initialTime ?? ""); // Provide default if undefined
|
||||
this.initialHour = hour;
|
||||
this.initialMinute = minute;
|
||||
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("time-picker-modal-content"); // For styling
|
||||
|
||||
contentEl.createEl("h2", { text: "Select Time" });
|
||||
|
||||
// --- Build Dropdowns ---
|
||||
this.uiElements = buildTimePickerDOM(contentEl, this.initialHour, this.initialMinute);
|
||||
|
||||
// --- Build Actions ---
|
||||
buildTimePickerActions(contentEl, this._handleCancel, this._handleConfirm);
|
||||
}
|
||||
|
||||
// --- Handlers ---
|
||||
|
||||
private _handleCancel = (): void => {
|
||||
this.close();
|
||||
};
|
||||
|
||||
private _handleConfirm = (): void => {
|
||||
if (!this.uiElements) {
|
||||
console.error("[TimePickerModal] Cannot confirm: UI elements not found.");
|
||||
return;
|
||||
}
|
||||
//? Get current values from dropdowns
|
||||
const selectedHour = this.uiElements.hourSelect.value;
|
||||
const selectedMinute = this.uiElements.minuteSelect.value;
|
||||
|
||||
//? Format the selected time
|
||||
const selectedTime = formatTimeString(selectedHour, selectedMinute);
|
||||
|
||||
this.onSelectCallback(selectedTime); // Pass HH:MM string back
|
||||
this.close();
|
||||
};
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
this.uiElements = null; // Clear references
|
||||
}
|
||||
}
|
||||
66
src/components/TimePickerModal/dom/buildTimePickerDOM.ts
Normal file
66
src/components/TimePickerModal/dom/buildTimePickerDOM.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Setting } from "obsidian";
|
||||
import { createHourOptions, createMinuteOptions } from "../timePickerUtils";
|
||||
|
||||
//? Interface for the references to the created select elements
|
||||
export interface TimePickerDOMElements {
|
||||
hourSelect: HTMLSelectElement;
|
||||
minuteSelect: HTMLSelectElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the core UI elements (dropdowns) for the Time Picker Modal.
|
||||
* @param contentEl - The HTMLElement to append the controls to.
|
||||
* @param initialHour - The initially selected hour (0-23).
|
||||
* @param initialMinute - The initially selected minute (0-59).
|
||||
* @returns Object containing references to the select elements.
|
||||
*/
|
||||
export function buildTimePickerDOM(
|
||||
contentEl: HTMLElement,
|
||||
initialHour: number,
|
||||
initialMinute: number
|
||||
): TimePickerDOMElements {
|
||||
|
||||
// Use Obsidian's Setting components for structure and alignment
|
||||
const setting = new Setting(contentEl)
|
||||
.setName("Select Time")
|
||||
.setClass("time-picker-controls"); // Add class for specific styling
|
||||
|
||||
// --- Hour Dropdown ---
|
||||
const hourSelect = document.createElement("select");
|
||||
hourSelect.classList.add("dropdown", "time-picker-select", "time-picker-hour");
|
||||
hourSelect.setAttribute("aria-label", "Hour");
|
||||
hourSelect.appendChild(createHourOptions(initialHour)); // Populate options
|
||||
setting.controlEl.appendChild(hourSelect); // Add to setting's control area
|
||||
|
||||
// --- Separator ---
|
||||
const separator = document.createElement("span");
|
||||
separator.textContent = ":";
|
||||
separator.addClass("time-picker-separator");
|
||||
setting.controlEl.appendChild(separator);
|
||||
|
||||
// --- Minute Dropdown ---
|
||||
const minuteSelect = document.createElement("select");
|
||||
minuteSelect.classList.add("dropdown", "time-picker-select", "time-picker-minute");
|
||||
minuteSelect.setAttribute("aria-label", "Minute");
|
||||
minuteSelect.appendChild(createMinuteOptions(initialMinute)); // Populate options
|
||||
setting.controlEl.appendChild(minuteSelect);
|
||||
|
||||
return { hourSelect, minuteSelect };
|
||||
}
|
||||
|
||||
/** Builds the action buttons (Cancel, Confirm) for the modal. */
|
||||
export function buildTimePickerActions(
|
||||
contentEl: HTMLElement,
|
||||
onCancel: () => void,
|
||||
onConfirm: () => void
|
||||
): void {
|
||||
new Setting(contentEl)
|
||||
.setClass("time-picker-actions") // Add class for styling
|
||||
.addButton(button => button
|
||||
.setButtonText("Cancel")
|
||||
.onClick(onCancel))
|
||||
.addButton(button => button
|
||||
.setButtonText("Confirm")
|
||||
.setCta() // Make it the primary action
|
||||
.onClick(onConfirm));
|
||||
}
|
||||
60
src/components/TimePickerModal/timePickerUtils.ts
Normal file
60
src/components/TimePickerModal/timePickerUtils.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
|
||||
/** Generates <option> elements for hours (00-23). */
|
||||
export function createHourOptions(selectedHour: number): DocumentFragment {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const option = document.createElement("option");
|
||||
const hourString = String(i).padStart(2, '0');
|
||||
option.value = hourString;
|
||||
option.textContent = hourString;
|
||||
if (i === selectedHour) {
|
||||
option.selected = true;
|
||||
}
|
||||
fragment.appendChild(option);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/** //? Generates <option> elements for minutes (00-59). */
|
||||
export function createMinuteOptions(selectedMinute: number): DocumentFragment {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const option = document.createElement("option");
|
||||
const minuteString = String(i).padStart(2, '0');
|
||||
option.value = minuteString;
|
||||
option.textContent = minuteString;
|
||||
if (i === selectedMinute) {
|
||||
option.selected = true;
|
||||
}
|
||||
fragment.appendChild(option);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/** //? Parses "HH:MM" string into hours and minutes numbers. */
|
||||
export function parseTimeString(timeString: string): { hour: number; minute: number } {
|
||||
let hour = 12; // Default hour
|
||||
let minute = 0; // Default minute
|
||||
|
||||
if (/^\d{1,2}:\d{1,2}$/.test(timeString)) {
|
||||
const parts = timeString.split(':').map(Number);
|
||||
const parsedHour = parts[0];
|
||||
const parsedMinute = parts[1];
|
||||
|
||||
if (!isNaN(parsedHour) && parsedHour >= 0 && parsedHour <= 23) {
|
||||
hour = parsedHour;
|
||||
}
|
||||
if (!isNaN(parsedMinute) && parsedMinute >= 0 && parsedMinute <= 59) {
|
||||
minute = parsedMinute;
|
||||
}
|
||||
}
|
||||
//& console.log(`[TimePickerUtils] Parsed "${timeString}" to H:${hour}, M:${minute}`);
|
||||
return { hour, minute };
|
||||
}
|
||||
|
||||
/** //? Formats hour and minute numbers into "HH:MM" string. */
|
||||
export function formatTimeString(hour: number | string, minute: number | string): string {
|
||||
const hh = String(hour).padStart(2, '0');
|
||||
const mm = String(minute).padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
54
src/styles/timePickerInject.ts
Normal file
54
src/styles/timePickerInject.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const STYLE_ID = "sqlite-db-time-picker-styles";
|
||||
|
||||
function getTimePickerStyles(): string {
|
||||
return `
|
||||
.time-picker-modal-content {
|
||||
padding: var(--size-4-4) var(--size-4-6);
|
||||
}
|
||||
.time-picker-modal-content h2 {
|
||||
text-align: center;
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
/* Style the setting containing the dropdowns */
|
||||
.time-picker-controls .setting-item-control {
|
||||
display: flex;
|
||||
justify-content: center; /* Center the dropdowns */
|
||||
align-items: center;
|
||||
gap: var(--size-4-1); /* Space between hour, :, minute */
|
||||
}
|
||||
.time-picker-select {
|
||||
min-width: 60px; /* Give dropdowns some width */
|
||||
text-align: center;
|
||||
}
|
||||
.time-picker-separator {
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: bold;
|
||||
line-height: var(--input-height); /* Align with dropdowns */
|
||||
padding: 0 var(--size-4-1);
|
||||
}
|
||||
/* Style the setting containing the buttons */
|
||||
.time-picker-actions {
|
||||
margin-top: var(--size-4-4);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
padding-top: var(--size-4-3);
|
||||
}
|
||||
.time-picker-actions .setting-item-control {
|
||||
justify-content: flex-end; /* Align buttons right */
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/** Injects CSS for the Time Picker modal into the document head. */
|
||||
export function injectTimePickerStyles(): void {
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = STYLE_ID;
|
||||
styleEl.textContent = getTimePickerStyles();
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
/** Removes the injected Time Picker styles. */
|
||||
export function removeTimePickerStyles(): void {
|
||||
const styleEl = document.getElementById(STYLE_ID);
|
||||
if (styleEl) styleEl.remove();
|
||||
}
|
||||
|
|
@ -11,15 +11,20 @@ import { upsertTextValue } from "./data/upsertTextValue";
|
|||
import { updateInputValue, setTextErrorState, clearTextErrorState, updateStaticTextUI } from "./dom/textInputUiUpdaters";
|
||||
import { DATE_CHANGED_EVENT_NAME } from "src/pluginState";
|
||||
|
||||
let textInputCounter = 0;
|
||||
|
||||
export class TextInput extends HTMLElement {
|
||||
// --- Dependencies ---
|
||||
private appInstance: App | null = null;
|
||||
public appInstance: App | null = null;
|
||||
public textDataService: TextInputDataService | null = null;
|
||||
|
||||
// --- State & Config ---
|
||||
public config: TextInputConfig = {};
|
||||
public currentValue: string = "";
|
||||
private _isInitialized: boolean = false;
|
||||
private _domBuilt: boolean = false; // Tracks if Shadow DOM exists
|
||||
private _initialLoadTriggered: boolean = false; // Tracks if initial load has been triggered
|
||||
|
||||
//^ data related state/config
|
||||
public table: string = "";
|
||||
public date: string = "";
|
||||
|
|
@ -28,54 +33,26 @@ export class TextInput extends HTMLElement {
|
|||
|
||||
private _isListeningForDateChanges: boolean = false; // Prevent multiple listeners
|
||||
|
||||
|
||||
// --- Shadow DOM Elements ---
|
||||
private uiElements!: TextInputDOMElements;
|
||||
private uniqueId: string; // For logging/debugging
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.uniqueId = `ti-${textInputCounter++}`;
|
||||
}
|
||||
|
||||
// --- Lifecycle ---
|
||||
connectedCallback() {
|
||||
//& console.log(`[TextInput ${this.uniqueId}] connectedCallback.`);
|
||||
//? We only mark that the element is in the DOM.
|
||||
//? Initialization waits for dependencies via setDependencies.
|
||||
//? Setup listener only after attributes are read.
|
||||
if (this._isInitialized) return;
|
||||
|
||||
//~ 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() {
|
||||
//& console.log(`[TextInput ${this.uniqueId}] disconnectedCallback - Cleaning up listener.`);
|
||||
if (this._isListeningForDateChanges) {
|
||||
document.removeEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
|
||||
this._isListeningForDateChanges = false;
|
||||
|
|
@ -85,66 +62,111 @@ export class TextInput extends HTMLElement {
|
|||
// --- Public Methods ---
|
||||
/** Injects DBService first, then App instance. */
|
||||
public setDependencies(dbService: DBService, app: App): void {
|
||||
if (!dbService || !app) { /* ... error ... */ return; }
|
||||
//& console.log(`[TextInput ${this.uniqueId}] setDependencies called.`);
|
||||
if (this._isInitialized) {
|
||||
//& console.log(`[TextInput ${this.uniqueId}] Already initialized, skipping dependency set.`);
|
||||
return; // Don't re-initialize if already done
|
||||
}
|
||||
if (!dbService || !app) {
|
||||
console.error("[TextInput] Invalid DBService or App instance provided during dependency injection.");
|
||||
this.textContent = "[Init Error - Dependencies]";
|
||||
return;
|
||||
}
|
||||
this.appInstance = app;
|
||||
try {
|
||||
this.textDataService = new TextInputDataService(dbService);
|
||||
} catch (error) { /* ... error ... */ return; }
|
||||
} catch (error) {
|
||||
console.error(`[TextInput ${this.uniqueId}] Failed to initialize TextInputDataService:`, error);
|
||||
this.textContent = "[Data Service Error]";
|
||||
return;
|
||||
}
|
||||
|
||||
//? Now that dependencies are set, complete the setup
|
||||
this._readAttributesAndInitializeData();
|
||||
//? Start the setup process now that dependencies are available
|
||||
this._readAttributesAndBuildConfig(); // Reads attributes, sets 'this.date', setups listener
|
||||
this._initializeComponent(); // Builds DOM, creates handlers, applies styles, triggers load
|
||||
this._isInitialized = true; // Mark initialization complete
|
||||
//& console.log(`[TextInput ${this.uniqueId}] Initialization fully complete.`);
|
||||
}
|
||||
|
||||
/** 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") ?? "";
|
||||
private _readAttributesAndBuildConfig(): void {
|
||||
// --- Read ALL Attributes ---
|
||||
this.config = {
|
||||
label: this.getAttribute("data-label") ?? undefined,
|
||||
placeholder: this.getAttribute("placeholder") ?? undefined,
|
||||
initialValueAttr: this.getAttribute("data-initial-value") ?? "",
|
||||
modalType: (this.getAttribute("data-modal-type") as ModalType) ?? 'none',
|
||||
isButton: this.getAttribute("data-is-button")?.toLowerCase() === 'true',
|
||||
table: this.getAttribute("data-table") ?? undefined,
|
||||
date: this.getAttribute("data-date") ?? "@date",
|
||||
valueCol: this.getAttribute("data-value-col") ?? undefined,
|
||||
dateCol: this.getAttribute("data-date-col") ?? undefined,
|
||||
};
|
||||
//? Copy to component properties
|
||||
this.table = this.config.table ?? "";
|
||||
this.date = this.config.date ?? "@date"; // Store initial date config here
|
||||
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;
|
||||
//^ --- Setup Listener AFTER reading attributes and setting this.date ---
|
||||
this._setupDateChangeListener(); // Safe to call now
|
||||
}
|
||||
|
||||
// --- Trigger Initial Load ---
|
||||
/** Builds DOM, creates handlers, applies styles, triggers initial load. */
|
||||
private _initializeComponent(): void {
|
||||
if (!this.appInstance) { console.error("[TextInput] Cannot initialize component: App instance missing."); return; }
|
||||
if (this.shadowRoot) { console.warn(`[TextInput ${this.config.label || this.uniqueId}] Already has shadowRoot, skipping DOM build.`); return } // Prevent rebuilding DOM
|
||||
|
||||
//? Pass 'this' which now contains the fully populated config
|
||||
const handlers = {
|
||||
handleInputChange: createInputChangeHandler(this),
|
||||
handleModalTrigger: createModalTriggerHandler(this),
|
||||
};
|
||||
|
||||
//~ Build DOM and get element references
|
||||
this.uiElements = buildTextInputDOM(this.attachShadow({ mode: "open" }), this.config, handlers);
|
||||
|
||||
//~ Apply styles
|
||||
applyTextInputStyles(this.shadowRoot!); // shadowRoot definitely exists now
|
||||
|
||||
this._domBuilt = true; // Mark DOM built
|
||||
|
||||
// --- Update Static UI & Trigger Initial Load (Deferred slightly) ---
|
||||
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?.();
|
||||
updateStaticTextUI(this.uiElements, this.config.label);
|
||||
|
||||
// --- Data Validation & Load Trigger ---
|
||||
if (this.table && this.valueCol && this.dateCol) {
|
||||
// Config looks okay for data operations
|
||||
if (!this._initialLoadTriggered) {
|
||||
this._initialLoadTriggered = true;
|
||||
loadTextValue(this).catch(err => console.error(`[TextInput ${this.config.label || this.uniqueId}] Unhandled error during initial load:`, err));
|
||||
}
|
||||
} else {
|
||||
console.warn(`[TextInput ${this.config.label || this.uniqueId}] Missing required data configuration attributes (table, valueCol, dateCol). Will use initialValueAttr.`);
|
||||
this._updateDisplay(this.config.initialValueAttr ?? "");
|
||||
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. */
|
||||
private _setValue = (newValue: string, triggerSave: boolean = false): void => {
|
||||
public _setValue = (newValue: string, triggerSave: boolean = false): void => {
|
||||
if (this.currentValue !== newValue) {
|
||||
this.currentValue = newValue;
|
||||
|
||||
//? Always update the input display
|
||||
this._updateDisplay(this.currentValue);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('input-change', { detail: { value: this.currentValue }, /*...*/ }));
|
||||
|
||||
//? Trigger save only if configured and requested
|
||||
if (triggerSave && this.config.table && this.config.date && this.config.valueCol && this.config.dateCol) {
|
||||
//? Call the upsert helper
|
||||
upsertTextValue(this).catch(err => {
|
||||
console.error(`[TextInput ${this.config.table}] Error during save:`, err);
|
||||
//? Consider reverting UI or showing persistent error?
|
||||
// this.showErrorState("Save Failed");
|
||||
});
|
||||
this._updateDisplay(this.currentValue); // Update UI optimistically
|
||||
this.dispatchEvent(new CustomEvent('input-change', { /* ... */ }));
|
||||
// Use 'this' properties for validation
|
||||
if (triggerSave && this.table && this.valueCol && this.dateCol) {
|
||||
//& console.log(`[TextInput ${this.config.label || this.uniqueId}] Save triggered.`);
|
||||
upsertTextValue(this).catch(/* ... */);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//? Listener Setup (called from _readAttributesAndBuildConfig)
|
||||
private _setupDateChangeListener(): void {
|
||||
const dateAttr = this.getAttribute("data-date"); // Read attribute directly
|
||||
if (dateAttr === "@date" && !this._isListeningForDateChanges) {
|
||||
if (this.date === "@date" && !this._isListeningForDateChanges) {
|
||||
document.addEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
|
||||
this._isListeningForDateChanges = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import type { TextInputEventProps } from "../TextInput.types";
|
||||
import type { TextInput } from "../TextInput";
|
||||
|
||||
export type InputChangeEventHandler = (event: Event) => void;
|
||||
|
||||
/** Creates the handler for the input element's 'change' event. */
|
||||
export function createInputChangeHandler(props: TextInputEventProps): InputChangeEventHandler {
|
||||
export function createInputChangeHandler(instance: TextInput): InputChangeEventHandler {
|
||||
return (event: Event) => {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
const newValue = event.target.value;
|
||||
props.setValue(newValue, true);
|
||||
//? Access component's method directly
|
||||
//? Pass true to trigger save on 'change' event
|
||||
instance._setValue(newValue, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,34 +1,41 @@
|
|||
import type { TextInputEventProps } from "../TextInput.types";
|
||||
import { TimePickerModal } from "../../../components/TimePickerModal";
|
||||
import { DatePickerModal } from "../../../codeblocks/dateHeader/components/datePicker/DatePickerModal";
|
||||
import type { TextInput } from "../TextInput";
|
||||
import { DatePickerModal } from "src/components/DatePickerModal/DatePickerModal";
|
||||
import { TimePickerModal } from "src/components/TimePickerModal/TimePickerModal";
|
||||
|
||||
export type ModalTriggerHandler = (event: MouseEvent) => void;
|
||||
|
||||
/** //? Creates the handler for the modal trigger element's 'click' event. */
|
||||
export function createModalTriggerHandler(props: TextInputEventProps): ModalTriggerHandler { // Use renamed type
|
||||
//^ Accept the component instance 'this' instead of props object
|
||||
export function createModalTriggerHandler(instance: TextInput): ModalTriggerHandler {
|
||||
return (event: MouseEvent) => {
|
||||
// No debug log needed at start
|
||||
const modalType = instance.config.modalType; // Get current modalType from config
|
||||
const app = instance.appInstance; // Get app instance
|
||||
const currentValue = instance.currentValue; // Get current value
|
||||
const label = instance.config.label; // Get label for logging
|
||||
|
||||
switch (props.modalType) {
|
||||
if (!app) {
|
||||
console.error(`[ModalTriggerHandler ${label}] Cannot open modal: App instance is missing.`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (modalType) {
|
||||
case 'time-picker':
|
||||
new TimePickerModal(props.app, props.currentValue, (selectedTime: string) => {
|
||||
props.setValue(selectedTime, true);
|
||||
new TimePickerModal(app, currentValue, (selectedTime: string) => {
|
||||
//? Use component's method to set value
|
||||
instance._setValue(selectedTime, true);
|
||||
}).open();
|
||||
break;
|
||||
|
||||
case 'date-picker':
|
||||
const initialDate = props.currentValue || new Date().toISOString().split('T')[0];
|
||||
new DatePickerModal(props.app, initialDate, (selectedDate: string) => {
|
||||
props.setValue(selectedDate, true);
|
||||
const initialDate = currentValue || new Date().toISOString().split('T')[0];
|
||||
new DatePickerModal(app, initialDate, (selectedDate: string) => {
|
||||
instance._setValue(selectedDate, true);
|
||||
}).open();
|
||||
break;
|
||||
|
||||
// todo: Add cases for 'list-select', 'custom', etc.
|
||||
|
||||
case 'none':
|
||||
default:
|
||||
// Do nothing or console.warn if unexpected click
|
||||
// console.warn(`[ModalTriggerHandler ${props.label}] Clicked trigger but modalType is '${props.modalType}'.`);
|
||||
console.warn(`[ModalTriggerHandler ${label}] Clicked trigger but modalType is '${modalType}'.`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue