mirror of
https://github.com/stfrigerio/sqliteDB.git
synced 2026-07-22 05:38:02 +00:00
added boolean switch
This commit is contained in:
parent
9c1020b8d3
commit
ad87e0e851
20 changed files with 705 additions and 16 deletions
5
main.ts
5
main.ts
|
|
@ -17,6 +17,7 @@ import { injectDatePickerStyles } from "src/styles/datePickerInject";
|
|||
import { injectDateNavigatorStyles, removeDateNavigatorStyles } from './src/styles/dateNavigationInject';
|
||||
|
||||
import { registerHabitCounter } from "./src/webcomponents/HabitCounter/registerHabitCounter";
|
||||
import { registerBooleanSwitch } from "src/webcomponents/BooleanSwitch/registerBooleanSwitch";
|
||||
|
||||
export default class SQLiteDBPlugin extends Plugin {
|
||||
settings: SQLiteDBSettings;
|
||||
|
|
@ -31,10 +32,13 @@ export default class SQLiteDBPlugin extends Plugin {
|
|||
injectDatePickerStyles();
|
||||
injectDateNavigatorStyles();
|
||||
|
||||
//? Components
|
||||
this.registerMarkdownPostProcessor((el, ctx) => {
|
||||
registerHabitCounter(el, this.dbService);
|
||||
registerBooleanSwitch(el, this.dbService);
|
||||
});
|
||||
|
||||
//? Commands
|
||||
this.addCommand({
|
||||
id: "inspect-table-structure",
|
||||
name: "Inspect table structure",
|
||||
|
|
@ -61,6 +65,7 @@ export default class SQLiteDBPlugin extends Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
//? Codeblocks
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"sql",
|
||||
async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
|
||||
|
|
|
|||
172
src/webcomponents/BooleanSwitch/BooleanSwitch.ts
Normal file
172
src/webcomponents/BooleanSwitch/BooleanSwitch.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { DBService } from "../../DBService";
|
||||
import { BooleanSwitchDataService } from "../services/BooleanDataService";
|
||||
import { applyBooleanSwitchStyles } from "./styles/applyBooleanSwitchStyles";
|
||||
import { buildBooleanSwitchDOM, BooleanSwitchUIElements } from "./dom/buildBooleanSwitchDOM";
|
||||
import { createChangeHandler } from "./eventHandlers/changeHandler";
|
||||
import { loadBooleanValue } from "./data/loadBooleanValue";
|
||||
import { upsertBooleanValue } from "./data/upsertBooleanValue";
|
||||
import {
|
||||
updateSwitchState, updateStaticBooleanUI, setBooleanSwitchErrorState,
|
||||
clearBooleanSwitchErrorState
|
||||
} from "./dom/booleanSwitchUiUpdaters";
|
||||
import { DATE_CHANGED_EVENT_NAME } from "../../pluginState";
|
||||
|
||||
//? Unique ID counter for label/input association
|
||||
let switchCounter = 0;
|
||||
|
||||
//? Web component for a boolean (0/1) toggle switch linked to database state.
|
||||
export class BooleanSwitch extends HTMLElement {
|
||||
// --- State ---
|
||||
public habitKey: string = "";
|
||||
public initialDate: string = "";
|
||||
public emoji: string = "";
|
||||
public table: string = "";
|
||||
public currentValue: 0 | 1 = 0; // Default state is 'off' (0)
|
||||
private _initialLoadTriggered: boolean = false;
|
||||
private _isInitialized: boolean = false;
|
||||
public habitIdCol: string = "";
|
||||
public valueCol: string = ""; // Column name for 'value' (0/1)
|
||||
public dateCol: string = "";
|
||||
|
||||
// --- Dependencies ---
|
||||
//? Renamed service property for clarity
|
||||
public booleanDataService: BooleanSwitchDataService | null = null;
|
||||
|
||||
// --- Shadow DOM Elements ---
|
||||
private uiElements!: BooleanSwitchUIElements;
|
||||
|
||||
private _isListeningForDateChanges: boolean = false; // Prevent multiple listeners
|
||||
|
||||
// --- Instance specific ID ---
|
||||
private uniqueId: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.uniqueId = `bs-${switchCounter++}`; // Assign unique ID
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this._isInitialized) return;
|
||||
|
||||
//~ Create bound event handler
|
||||
const changeHandler = createChangeHandler(this);
|
||||
|
||||
this.uiElements = buildBooleanSwitchDOM(this.attachShadow({ mode: "open" }), this.uniqueId, changeHandler);
|
||||
applyBooleanSwitchStyles(this.shadowRoot!);
|
||||
this._isInitialized = true;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this._isListeningForDateChanges) {
|
||||
document.removeEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
|
||||
this._isListeningForDateChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
//? Inject DBService and trigger attribute reading/initial load
|
||||
public setDbService(service: DBService): void {
|
||||
if (!service) { /* ... error handling ... */ this.showErrorState("Setup Error"); return; }
|
||||
try {
|
||||
this.booleanDataService = new BooleanSwitchDataService(service);
|
||||
} catch (error) { /* ... error handling ... */ this.showErrorState("Setup Error"); return; }
|
||||
this._readAttributesAndInitLoad();
|
||||
}
|
||||
|
||||
private _readAttributesAndInitLoad(): void {
|
||||
// --- Read ALL attributes ---
|
||||
this.habitKey = this.getAttribute("data-key") ?? "";
|
||||
this.initialDate = this.getAttribute("data-date") ?? "@date";
|
||||
this.emoji = this.getAttribute("data-emoji") ?? "❓";
|
||||
this.table = this.getAttribute("data-table") ?? "";
|
||||
this.habitIdCol = this.getAttribute("data-key-id-col") ?? "";
|
||||
this.valueCol = this.getAttribute("data-value-col") ?? "";
|
||||
this.dateCol = this.getAttribute("data-date-col") ?? "";
|
||||
|
||||
this._setupDateChangeListener();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
updateStaticBooleanUI(this.uiElements, this.emoji, this.habitKey); //~ Update static parts of UI
|
||||
|
||||
if (!this.table || !this.habitKey || !this.habitIdCol || !this.valueCol || !this.dateCol) {
|
||||
console.warn(`[HabitCounter Component ${this.habitKey}] Missing one or more required attributes: table, habit, data-habit-id-col, data-value-col, data-date-col.`);
|
||||
this.showErrorState("Config Error");
|
||||
updateStaticBooleanUI(this.uiElements, this.emoji, this.habitKey || "Config Error");
|
||||
return; // Stop initialization if config is bad
|
||||
}
|
||||
|
||||
if (!this._initialLoadTriggered) {
|
||||
this._initialLoadTriggered = true;
|
||||
loadBooleanValue(this).catch(err => console.error("Unhandled error during initial load:", err)); //~ Load data via helper
|
||||
} else {
|
||||
console.log(`[BooleanSwitch Component ${this.habitKey}] Initial load already triggered.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _setupDateChangeListener(): void {
|
||||
//? Only add listener if component uses dynamic date and not already listening
|
||||
if (this.initialDate === "@date" && !this._isListeningForDateChanges) {
|
||||
document.addEventListener(DATE_CHANGED_EVENT_NAME, this._handleGlobalDateChange);
|
||||
this._isListeningForDateChanges = true;
|
||||
} else {
|
||||
//& console.log(`[BooleanSwitch ${this.habitKey}] Not adding listener (date=${this.initialDate}, listening=${this._isListeningForDateChanges})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Inside BooleanSwitch class
|
||||
private _handleGlobalDateChange = (event: Event): void => {
|
||||
//? Type guard for CustomEvent
|
||||
if (!(event instanceof CustomEvent)) {
|
||||
console.log(`[BooleanSwitch ${this.habitKey}] Event was not CustomEvent.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newIsoDate = event.detail?.newIsoDate;
|
||||
|
||||
//? Double-check this component should react (uses @date)
|
||||
if (this.initialDate === "@date") {
|
||||
loadBooleanValue(this)
|
||||
.catch(err => {
|
||||
console.error(`[BooleanSwitch ${this.habitKey}] Error reloading data after date change:`, err);
|
||||
})
|
||||
} else {
|
||||
//& console.log(`[BooleanSwitch ${this.habitKey}] Ignoring date change event (date=${this.initialDate}).`);
|
||||
}
|
||||
};
|
||||
|
||||
//? Public method called by change handler to trigger DB update
|
||||
public async _updateData(): Promise<void> {
|
||||
await upsertBooleanValue(this);
|
||||
}
|
||||
|
||||
//? Public method called by data helpers to update display state
|
||||
public _updateDisplay(newValue: 0 | 1): void {
|
||||
this.currentValue = newValue;
|
||||
updateSwitchState(this.uiElements, newValue === 1);
|
||||
this.clearErrorState();
|
||||
this._enableSwitch();
|
||||
}
|
||||
|
||||
//? Public methods for UI state helpers
|
||||
public showErrorState(message?: string): void {
|
||||
setBooleanSwitchErrorState(this, this.uiElements, message); // Pass host element
|
||||
}
|
||||
public clearErrorState(): void {
|
||||
clearBooleanSwitchErrorState(this, this.uiElements);
|
||||
}
|
||||
|
||||
//? Enable/disable the actual input element
|
||||
private _enableSwitch(): void {
|
||||
if(this.uiElements.checkboxElement) this.uiElements.checkboxElement.disabled = false;
|
||||
this.removeAttribute('aria-disabled');
|
||||
}
|
||||
private _disableSwitch(): void {
|
||||
if(this.uiElements.checkboxElement) this.uiElements.checkboxElement.disabled = true;
|
||||
this.setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Custom Element Definition ---
|
||||
if (!customElements.get("boolean-switch")) {
|
||||
customElements.define("boolean-switch", BooleanSwitch);
|
||||
}
|
||||
19
src/webcomponents/BooleanSwitch/BooleanSwitch.types.ts
Normal file
19
src/webcomponents/BooleanSwitch/BooleanSwitch.types.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//? Expected structure from DB fetch (aliased)
|
||||
export interface BooleanRecord {
|
||||
value: 0 | 1; //? Expecting 0 or 1 specifically
|
||||
}
|
||||
|
||||
//? Base arguments for identifying an entry
|
||||
export interface BooleanSwitchDataArgs {
|
||||
table: string;
|
||||
habitKey: string;
|
||||
date: string;
|
||||
habitIdCol: string; //? Column name for the 'habitKey'
|
||||
valueCol: string; //? Column name for the 0/1 value
|
||||
dateCol: string;
|
||||
}
|
||||
|
||||
//? Arguments for upserting the boolean value
|
||||
export interface UpsertBooleanSwitchArgs extends BooleanSwitchDataArgs {
|
||||
newValue: 0 | 1; //? The new state (0 or 1)
|
||||
}
|
||||
40
src/webcomponents/BooleanSwitch/data/loadBooleanValue.ts
Normal file
40
src/webcomponents/BooleanSwitch/data/loadBooleanValue.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { BooleanSwitchDataService } from "../../services/BooleanDataService";
|
||||
import { calculateEffectiveDate } from "../../services/utils/calculateEffectiveDate";
|
||||
|
||||
//? Interface describing the component instance parts needed for loading
|
||||
interface LoadBooleanComponentInstance {
|
||||
booleanDataService: BooleanSwitchDataService | null;
|
||||
table: string; habitKey: string; initialDate: string;
|
||||
habitIdCol: string; valueCol: string; dateCol: string;
|
||||
_updateDisplay: (value: 0 | 1) => void; // Expects 0 or 1
|
||||
showErrorState: (message: string) => void;
|
||||
}
|
||||
|
||||
/** Orchestrates loading the boolean value using the BooleanSwitchDataService. */
|
||||
export async function loadBooleanValue(instance: LoadBooleanComponentInstance): Promise<void> {
|
||||
if (!instance.booleanDataService || !instance.table || !instance.habitKey || !instance.habitIdCol || !instance.valueCol || !instance.dateCol) {
|
||||
console.error(`[LoadBooleanValue ${instance.habitKey}] Cannot load data: Missing required properties.`);
|
||||
instance.showErrorState("Load Error - Config");
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveDate = calculateEffectiveDate(instance.initialDate);
|
||||
|
||||
const args = {
|
||||
table: instance.table,
|
||||
habitKey: instance.habitKey,
|
||||
date: effectiveDate,
|
||||
habitIdCol: instance.habitIdCol,
|
||||
valueCol: instance.valueCol,
|
||||
dateCol: instance.dateCol,
|
||||
};
|
||||
|
||||
try {
|
||||
const value = await instance.booleanDataService.fetchBooleanValue(args);
|
||||
instance._updateDisplay(value);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[LoadBooleanValue ${instance.habitKey}] Failed to load data:`, error);
|
||||
instance.showErrorState("Load Error - DB");
|
||||
}
|
||||
}
|
||||
49
src/webcomponents/BooleanSwitch/data/upsertBooleanValue.ts
Normal file
49
src/webcomponents/BooleanSwitch/data/upsertBooleanValue.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { BooleanSwitchDataService } from "../../services/BooleanDataService";
|
||||
import { calculateEffectiveDate } from "../../services/utils/calculateEffectiveDate";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
//? Interface describing the component instance parts needed for upserting
|
||||
interface UpsertBooleanComponentInstance {
|
||||
booleanDataService: BooleanSwitchDataService | null;
|
||||
table: string; habitKey: string; initialDate: string;
|
||||
currentValue: 0 | 1;
|
||||
habitIdCol: string; valueCol: string; dateCol: string;
|
||||
_updateDisplay: (value: 0 | 1) => void;
|
||||
showErrorState: (message: string) => void;
|
||||
clearErrorState: () => void;
|
||||
}
|
||||
|
||||
/** Orchestrates upserting the boolean value via the BooleanSwitchDataService. */
|
||||
export async function upsertBooleanValue(instance: UpsertBooleanComponentInstance): Promise<void> {
|
||||
if (!instance.booleanDataService || !instance.table || !instance.habitKey || !instance.habitIdCol || !instance.valueCol || !instance.dateCol) {
|
||||
console.error(`[UpsertBooleanValue ${instance.habitKey}] Cannot update data: Missing required properties.`);
|
||||
instance.showErrorState("Save Error - Config");
|
||||
new Notice("Cannot update switch: Configuration incomplete.");
|
||||
return;
|
||||
}
|
||||
|
||||
//^ Flip the current value (0 becomes 1, 1 becomes 0)
|
||||
const newValue = instance.currentValue === 1 ? 0 : 1;
|
||||
|
||||
const args = {
|
||||
table: instance.table,
|
||||
habitKey: instance.habitKey,
|
||||
date: calculateEffectiveDate(instance.initialDate),
|
||||
newValue: newValue as 0 | 1,
|
||||
habitIdCol: instance.habitIdCol,
|
||||
valueCol: instance.valueCol,
|
||||
dateCol: instance.dateCol,
|
||||
};
|
||||
|
||||
try {
|
||||
await instance.booleanDataService.upsertBooleanValue(args);
|
||||
instance._updateDisplay(newValue); // Update UI with the new value
|
||||
instance.clearErrorState();
|
||||
} catch (error) {
|
||||
console.error(`[UpsertBooleanValue ${instance.habitKey}] Failed to upsert data:`, error);
|
||||
instance.showErrorState("Save Error - DB");
|
||||
new Notice(`Error saving switch state for ${instance.habitKey}`);
|
||||
//? Optional: Revert display on error?
|
||||
// instance._updateDisplay(instance.currentValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { BooleanSwitchUIElements } from "./buildBooleanSwitchDOM";
|
||||
|
||||
/** Updates the checked state of the switch input. */
|
||||
export function updateSwitchState(elements: BooleanSwitchUIElements, isChecked: boolean): void {
|
||||
if (elements.checkboxElement) {
|
||||
elements.checkboxElement.checked = isChecked;
|
||||
elements.checkboxElement.setAttribute("aria-checked", String(isChecked));
|
||||
} else {
|
||||
console.error("[BooleanSwitchUI] checkboxElement not found for state update.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates label text based on component state. */
|
||||
export function updateStaticBooleanUI(elements: BooleanSwitchUIElements, emoji: string, key: string): void {
|
||||
if (elements.labelElement) {
|
||||
elements.labelElement.textContent = `${emoji} ${key}:`;
|
||||
} else {
|
||||
console.error("[BooleanSwitchUI] labelElement not found for static update.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the component to visually indicate an error state and disables the switch. */
|
||||
export function setBooleanSwitchErrorState(hostElement: HTMLElement, elements: BooleanSwitchUIElements, message: string = "Error"): void {
|
||||
if (elements.checkboxElement) {
|
||||
elements.checkboxElement.disabled = true;
|
||||
}
|
||||
hostElement.classList.add('error'); // Add error class to host for potential styling
|
||||
hostElement.setAttribute('aria-disabled', 'true');
|
||||
hostElement.setAttribute('title', message); // Tooltip for error details
|
||||
}
|
||||
|
||||
/** Clears any visual error indication and enables the switch. */
|
||||
export function clearBooleanSwitchErrorState(hostElement: HTMLElement, elements: BooleanSwitchUIElements): void {
|
||||
if (elements.checkboxElement) {
|
||||
elements.checkboxElement.disabled = false;
|
||||
}
|
||||
hostElement.classList.remove('error');
|
||||
hostElement.removeAttribute('aria-disabled');
|
||||
hostElement.removeAttribute('title');
|
||||
}
|
||||
46
src/webcomponents/BooleanSwitch/dom/buildBooleanSwitchDOM.ts
Normal file
46
src/webcomponents/BooleanSwitch/dom/buildBooleanSwitchDOM.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import type { ChangeEventHandler } from "../eventHandlers/changeHandler"; // Import type
|
||||
|
||||
//? Type defining the UI Elements specific to the BooleanSwitch
|
||||
export interface BooleanSwitchUIElements {
|
||||
wrapper: HTMLElement; // The main host element might act as wrapper
|
||||
labelElement: HTMLLabelElement; // Use label for accessibility
|
||||
checkboxElement: HTMLInputElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Shadow DOM structure for the boolean switch.
|
||||
* @param shadowRoot - The ShadowRoot to append elements to.
|
||||
* @param uniqueId - A unique ID for linking label and input.
|
||||
* @param changeHandler - The function to call when the checkbox state changes.
|
||||
* @returns An object containing references to the created UI elements.
|
||||
*/
|
||||
export function buildBooleanSwitchDOM(
|
||||
shadowRoot: ShadowRoot,
|
||||
uniqueId: string, // Needed for label 'for' attribute
|
||||
changeHandler: ChangeEventHandler
|
||||
): BooleanSwitchUIElements {
|
||||
if (!shadowRoot) throw new Error("Cannot build DOM: ShadowRoot is null.");
|
||||
|
||||
const wrapper = document.createElement("div"); // Use an inner wrapper if needed, or style host directly
|
||||
wrapper.className = "switch-wrapper";
|
||||
|
||||
// --- Checkbox Input (The actual switch mechanism) ---
|
||||
const checkboxElement = document.createElement("input");
|
||||
checkboxElement.type = "checkbox";
|
||||
checkboxElement.id = `bool-switch-${uniqueId}`; // Unique ID
|
||||
checkboxElement.className = "bool-switch-input";
|
||||
checkboxElement.setAttribute("role", "switch"); // ARIA role
|
||||
checkboxElement.addEventListener("change", changeHandler);
|
||||
|
||||
// --- Label (Emoji + Text, associated with checkbox) ---
|
||||
const labelElement = document.createElement("label");
|
||||
labelElement.htmlFor = checkboxElement.id; // Link label to input
|
||||
labelElement.className = "switch-label";
|
||||
//? Text content will be set by the component later
|
||||
|
||||
//? Append in desired visual order (Label first, then Switch)
|
||||
wrapper.append(labelElement, checkboxElement);
|
||||
shadowRoot.append(wrapper);
|
||||
|
||||
return { wrapper, labelElement, checkboxElement };
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
//? Type for the component instance
|
||||
interface BooleanSwitchInstance {
|
||||
_updateData: () => Promise<void>;
|
||||
habitKey: string;
|
||||
}
|
||||
|
||||
//? Type for the event handler function itself
|
||||
export type ChangeEventHandler = (event: Event) => void;
|
||||
|
||||
/** Creates the handler for the checkbox's 'change' event. */
|
||||
export function createChangeHandler(instance: BooleanSwitchInstance): ChangeEventHandler {
|
||||
return (event: Event) => {
|
||||
//? We don't actually need the event target's value,
|
||||
//? the component's _updateData method will flip the *current* state.
|
||||
instance._updateData().catch(err => {
|
||||
//? Error logging, specific handling is inside _updateData
|
||||
console.error(`[BooleanSwitchChangeHandler ${instance.habitKey}] Error during update:`, err);
|
||||
//? Consider reverting the checkbox state visually if update fails? Requires more complex state management.
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
// Example: Revert visual state - CAUTION: might fight with error state UI
|
||||
event.target.checked = !event.target.checked;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
58
src/webcomponents/BooleanSwitch/registerBooleanSwitch.ts
Normal file
58
src/webcomponents/BooleanSwitch/registerBooleanSwitch.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { DBService } from "../../DBService";
|
||||
import { BooleanSwitch } from "./BooleanSwitch";
|
||||
|
||||
//? Define element if needed
|
||||
if (!customElements.get("boolean-switch")) {
|
||||
customElements.define("boolean-switch", BooleanSwitch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds placeholder elements and replaces them with initialized BooleanSwitch components.
|
||||
* @param el The container element to search within.
|
||||
* @param dbService The DBService instance for dependency injection.
|
||||
*/
|
||||
export const registerBooleanSwitch = (el: HTMLElement, dbService: DBService) => {
|
||||
const placeholders = el.querySelectorAll("span.boolean-switch-placeholder");
|
||||
|
||||
placeholders.forEach((placeholderEl) => {
|
||||
if (!(placeholderEl instanceof HTMLElement)) return;
|
||||
|
||||
//~ Read all expected attributes from the placeholder
|
||||
const habitKey = placeholderEl.dataset.habit;
|
||||
const date = placeholderEl.dataset.date;
|
||||
const emoji = placeholderEl.dataset.emoji;
|
||||
const table = placeholderEl.dataset.table;
|
||||
const habitIdCol = placeholderEl.dataset.habitIdCol;
|
||||
const valueCol = placeholderEl.dataset.valueCol;
|
||||
const dateCol = placeholderEl.dataset.dateCol;
|
||||
|
||||
// --- Basic Validation ---
|
||||
if (!habitKey || !table || !habitIdCol || !valueCol || !dateCol) {
|
||||
console.warn("[BooleanSwitch Reg] Placeholder missing one or more required data attributes (habitKey, table, habitIdCol, valueCol, dateCol):", placeholderEl);
|
||||
placeholderEl.textContent = "[Config Error]";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const component = document.createElement("boolean-switch") as BooleanSwitch;
|
||||
|
||||
//~ Set all attributes on the component element
|
||||
component.setAttribute("data-key", habitKey);
|
||||
component.setAttribute("data-table", table);
|
||||
if (date) component.setAttribute("data-date", date);
|
||||
else component.setAttribute("data-date", "@date"); // Default to dynamic date
|
||||
if (emoji) component.setAttribute("data-emoji", emoji);
|
||||
component.setAttribute("data-key-id-col", habitIdCol);
|
||||
component.setAttribute("data-value-col", valueCol);
|
||||
component.setAttribute("data-date-col", dateCol);
|
||||
|
||||
component.setDbService(dbService);
|
||||
|
||||
placeholderEl.replaceWith(component);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[BooleanSwitch Reg] Error processing placeholder for key "${habitKey}":`, error, placeholderEl);
|
||||
placeholderEl.textContent = "[Error Loading]";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { getBooleanSwitchStyles } from "./getBooleanSwitchStyles";
|
||||
|
||||
/** Creates a style element and appends it to the shadow root. */
|
||||
export function applyBooleanSwitchStyles(shadowRoot: ShadowRoot): void {
|
||||
if (!shadowRoot) {
|
||||
console.error("[BooleanSwitchStyles] Cannot apply styles: ShadowRoot is null.");
|
||||
return;
|
||||
}
|
||||
const style = document.createElement("style");
|
||||
style.textContent = getBooleanSwitchStyles();
|
||||
shadowRoot.appendChild(style);
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/** Returns the CSS string for the BooleanSwitch component's Shadow DOM. */
|
||||
export function getBooleanSwitchStyles(): string {
|
||||
return `
|
||||
:host {
|
||||
display: inline-flex; /* Align items horizontally */
|
||||
align-items: center;
|
||||
gap: 0.6em; /* Space between label and switch */
|
||||
font-family: var(--font-text);
|
||||
cursor: pointer; /* Make the whole thing clickable */
|
||||
user-select: none; /* Prevent text selection */
|
||||
}
|
||||
|
||||
.switch-wrapper {
|
||||
display: contents; /* Don't add extra layout */
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
/* Styles for the emoji + text label */
|
||||
white-space: nowrap;
|
||||
color: var(--text-normal);
|
||||
line-height: 1.2; /* Align better with switch */
|
||||
}
|
||||
|
||||
/* Basic Checkbox Styling (Foundation) */
|
||||
input[type="checkbox"] {
|
||||
appearance: none; /* Remove default OS styling */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
position: relative; /* For positioning pseudo-elements */
|
||||
width: 36px; /* Width of the switch */
|
||||
height: 20px; /* Height of the switch */
|
||||
background-color: var(--background-modifier-border); /* Off state background */
|
||||
border-radius: 10px; /* Rounded corners */
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
vertical-align: middle; /* Align with text */
|
||||
margin: 0; /* Remove default margin */
|
||||
flex-shrink: 0; /* Prevent shrinking */
|
||||
}
|
||||
|
||||
/* Styling the 'thumb' or 'knob' of the switch */
|
||||
input[type="checkbox"]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px; /* Size of the knob */
|
||||
height: 16px;
|
||||
background-color: var(--background-primary); /* Knob color */
|
||||
border-radius: 50%; /* Make it round */
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Styling when the switch is checked (On state) */
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: var(--interactive-accent); /* Use accent color for on state */
|
||||
}
|
||||
|
||||
/* Moving the knob when checked */
|
||||
input[type="checkbox"]:checked::before {
|
||||
transform: translateX(16px); /* Move knob to the right */
|
||||
}
|
||||
|
||||
/* Focus state for accessibility */
|
||||
input[type="checkbox"]:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--background-primary), 0 0 0 4px var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
input[type="checkbox"]:disabled {
|
||||
background-color: var(--background-modifier-border-hover);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
input[type="checkbox"]:disabled::before {
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
:host([aria-disabled="true"]) { /* Style host when disabled */
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Error state indication (optional, applied to host or wrapper) */
|
||||
:host(.error) {
|
||||
/* e.g., outline: 1px solid var(--text-error); */
|
||||
/* e.g., border-radius: var(--radius-s); */
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { DBService } from "../../DBService";
|
||||
import { HabitDataService } from "./services/HabitDataService";
|
||||
import { HabitDataService } from "../services/HabitDataService";
|
||||
import { applyHabitCounterStyles } from "./styles/applyHabitCounterStyles";
|
||||
import { buildHabitCounterDOM } from "./dom/buildHabitCounterDOM";
|
||||
import { createDecrementHandler, createIncrementHandler } from "./eventHandlers/clickHandlers";
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
clearComponentErrorState,
|
||||
HabitCounterUIElements
|
||||
} from "./dom/uiUpdaters";
|
||||
import { DATE_CHANGED_EVENT_NAME, pluginState } from "../../pluginState";
|
||||
import { DATE_CHANGED_EVENT_NAME, } from "../../pluginState";
|
||||
|
||||
//? Main Web Component class, acts as an orchestrator.
|
||||
export class HabitCounter extends HTMLElement {
|
||||
|
|
@ -33,9 +33,7 @@ export class HabitCounter extends HTMLElement {
|
|||
|
||||
// --- Shadow DOM Element References ---
|
||||
//? Store the object returned by the builder
|
||||
private uiElements: HabitCounterUIElements = {
|
||||
wrapper: null, valueDisplay: null, labelElement: null, minusButton: null, plusButton: null
|
||||
};
|
||||
private uiElements: HabitCounterUIElements
|
||||
|
||||
private _isListeningForDateChanges: boolean = false; // Prevent multiple listeners
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HabitDataService } from "../services/HabitDataService";
|
||||
import { calculateEffectiveDate } from "./calculateEffectiveDate";
|
||||
import { HabitDataService } from "../../services/HabitDataService";
|
||||
import { calculateEffectiveDate } from "../../services/utils/calculateEffectiveDate";
|
||||
|
||||
//? Interface describing the necessary parts of the component instance for loading.
|
||||
interface LoadComponentInstance {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HabitDataService } from "../services/HabitDataService";
|
||||
import { calculateEffectiveDate } from "./calculateEffectiveDate";
|
||||
import { HabitDataService } from "../../services/HabitDataService";
|
||||
import { calculateEffectiveDate } from "../../services/utils/calculateEffectiveDate";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
//? Interface describing the necessary parts of the component instance for updating.
|
||||
|
|
|
|||
94
src/webcomponents/services/BooleanDataService.ts
Normal file
94
src/webcomponents/services/BooleanDataService.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { DBService } from "../../DBService";
|
||||
import { BooleanRecord, BooleanSwitchDataArgs, UpsertBooleanSwitchArgs } from "../BooleanSwitch/BooleanSwitch.types";
|
||||
import { quoteSqlIdentifier } from "./utils/quoteSqlIdentifier";
|
||||
import { validateBaseBooleanArgs, validateUpsertBooleanArgs } from "./utils/validateBooleanArgs";
|
||||
|
||||
export class BooleanSwitchDataService {
|
||||
private dbService: DBService;
|
||||
constructor(dbService: DBService) {
|
||||
if (!dbService) throw new Error("BooleanSwitchDataService requires a valid DBService instance.");
|
||||
this.dbService = dbService;
|
||||
}
|
||||
|
||||
/** Fetches the current 0/1 value for a specific key/date. Returns 0 if not found. */
|
||||
async fetchBooleanValue(args: BooleanSwitchDataArgs): Promise<0 | 1> {
|
||||
try {
|
||||
validateBaseBooleanArgs(args);
|
||||
const safeTable = quoteSqlIdentifier(args.table);
|
||||
const safeKeyIdCol = quoteSqlIdentifier(args.habitIdCol);
|
||||
const safeValueCol = quoteSqlIdentifier(args.valueCol);
|
||||
const safeDateCol = quoteSqlIdentifier(args.dateCol);
|
||||
|
||||
const sql = `SELECT ${safeValueCol} AS value FROM ${safeTable} WHERE ${safeKeyIdCol} = ? AND ${safeDateCol} = ?`;
|
||||
const params = [args.habitKey, args.date];
|
||||
|
||||
const result = await this.dbService.getQuery<BooleanRecord>(sql, params);
|
||||
|
||||
const value = result?.[0]?.value;
|
||||
//^ Return 1 if value is 1, otherwise default to 0
|
||||
return value === 1 ? 1 : 0;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[BoolDataService] Error in fetchBooleanValue for ${args.habitKey}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Upserts the 0/1 value. Handles optional UUID column logic. */
|
||||
async upsertBooleanValue(args: UpsertBooleanSwitchArgs): Promise<void> {
|
||||
try {
|
||||
validateUpsertBooleanArgs(args);
|
||||
|
||||
const safeTable = quoteSqlIdentifier(args.table);
|
||||
const safeHabitIdCol = quoteSqlIdentifier(args.habitIdCol);
|
||||
const safeValueCol = quoteSqlIdentifier(args.valueCol);
|
||||
const safeDateCol = quoteSqlIdentifier(args.dateCol);
|
||||
|
||||
let sql = "";
|
||||
let params: (string | number)[] = [];
|
||||
|
||||
let existingUuid: string | undefined;
|
||||
|
||||
try {
|
||||
//^ Attempt to fetch UUID. Assumes the column is named 'uuid' if used.
|
||||
const fetchUuidSql = `SELECT uuid FROM ${safeTable} WHERE ${safeHabitIdCol} = ? AND ${safeDateCol} = ?`;
|
||||
const uuidResult = await this.dbService.getQuery<{ uuid: string }>(fetchUuidSql, [args.habitKey, args.date]);
|
||||
existingUuid = uuidResult?.[0]?.uuid;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && /no such column|does not exist/i.test(error.message) && error.message.includes('uuid')) {
|
||||
console.log(`[BoolDataService] Info: Optional 'uuid' column not found in table '${args.table}'. Proceeding with standard upsert.`);
|
||||
existingUuid = undefined; // Ensure it's undefined so the INSERT path is taken
|
||||
} else {
|
||||
console.error(`[BoolDataService] Error fetching potential UUID for ${args.habitKey} on ${args.date}:`, error);
|
||||
throw error; // Re-throw unexpected errors
|
||||
}
|
||||
}
|
||||
|
||||
if (existingUuid) {
|
||||
sql = `UPDATE ${safeTable} SET ${safeValueCol} = ? WHERE uuid = ?`;
|
||||
params = [args.newValue, existingUuid];
|
||||
} else {
|
||||
// Standard UPSERT path (no UUID column configured)
|
||||
sql = `
|
||||
INSERT INTO ${safeTable} (${safeHabitIdCol}, ${safeDateCol}, ${safeValueCol})
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(${safeHabitIdCol}, ${safeDateCol}) DO UPDATE
|
||||
SET ${safeValueCol} = excluded.${safeValueCol};
|
||||
`;
|
||||
//! Requires UNIQUE constraint on (keyIdCol, dateCol)
|
||||
params = [args.habitKey, args.date, args.newValue];
|
||||
}
|
||||
|
||||
await this.dbService.runQuery(sql, params);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[BoolDataService] Error in upsertBooleanValue for ${args.habitKey}:`, error);
|
||||
if (error instanceof Error && error.message.includes("ON CONFLICT clause does not match")) {
|
||||
//^ Provide more specific guidance mentioning the user-provided columns
|
||||
console.error(`//! DB Constraint Error: Table '${args.table}' needs a UNIQUE index/key on (${args.habitIdCol}, ${args.dateCol}). Run: CREATE UNIQUE INDEX IF NOT EXISTS idx_habit_date ON ${quoteSqlIdentifier(args.table)} (${quoteSqlIdentifier(args.habitIdCol)}, ${quoteSqlIdentifier(args.dateCol)});`);
|
||||
throw new Error(`Missing UNIQUE constraint on (${args.habitIdCol}, ${args.dateCol}) in table ${args.table}. ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { DBService } from "../../../DBService";
|
||||
import { HabitRecord, HabitDataArgs, UpdateHabitDataArgs } from "../HabitCounter.types";
|
||||
import { DBService } from "../../DBService";
|
||||
import { HabitRecord, HabitDataArgs, UpdateHabitDataArgs } from "../HabitCounter/HabitCounter.types";
|
||||
import { quoteSqlIdentifier } from "./utils/quoteSqlIdentifier";
|
||||
import { validateFetchArgs, validateUpdateArgs } from "./utils/validateHabitArgs";
|
||||
|
||||
|
|
@ -50,12 +50,25 @@ export class HabitDataService {
|
|||
const safeValueCol = quoteSqlIdentifier(args.valueCol);
|
||||
const safeDateCol = quoteSqlIdentifier(args.dateCol);
|
||||
|
||||
//? Attempt to fetch existing UUID
|
||||
const fetchUuidSql = `SELECT uuid FROM ${safeTable} WHERE ${safeHabitIdCol} = ? AND ${safeDateCol} = ?`;
|
||||
const fetchUuidParams = [args.habitKey, args.date];
|
||||
const uuidResult = await this.dbService.getQuery<UuidResult>(fetchUuidSql, fetchUuidParams);
|
||||
const existingUuid = uuidResult?.[0]?.uuid;
|
||||
let existingUuid: string | undefined;
|
||||
|
||||
//? Attempt to fetch existing UUID
|
||||
try {
|
||||
//^ Attempt to fetch UUID. Assumes the column is named 'uuid' if used.
|
||||
const fetchUuidSql = `SELECT uuid FROM ${safeTable} WHERE ${safeHabitIdCol} = ? AND ${safeDateCol} = ?`;
|
||||
const uuidResult = await this.dbService.getQuery<{ uuid: string }>(fetchUuidSql, [args.habitKey, args.date]);
|
||||
existingUuid = uuidResult?.[0]?.uuid;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && /no such column|does not exist/i.test(error.message) && error.message.includes('uuid')) {
|
||||
//^ It's expected that 'uuid' might not exist. Log for info and proceed without UUID.
|
||||
console.log(`[HabitDataService] Info: Optional 'uuid' column not found in table '${args.table}'. Proceeding with standard upsert.`);
|
||||
existingUuid = undefined; // Ensure it's undefined so the INSERT path is taken
|
||||
} else {
|
||||
//^ A different, unexpected error occurred during the UUID fetch, re-throw it.
|
||||
console.error(`[HabitDataService] Error fetching potential UUID for ${args.habitKey} on ${args.date}:`, error);
|
||||
throw error; // Re-throw unexpected errors
|
||||
}
|
||||
}
|
||||
|
||||
if (existingUuid && typeof existingUuid === 'string') {
|
||||
const updateSql = `UPDATE ${safeTable} SET ${safeValueCol} = ? WHERE uuid = ?`;
|
||||
27
src/webcomponents/services/utils/validateBooleanArgs.ts
Normal file
27
src/webcomponents/services/utils/validateBooleanArgs.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { BooleanSwitchDataArgs, UpsertBooleanSwitchArgs } from "../../BooleanSwitch/BooleanSwitch.types";
|
||||
|
||||
const IDENTIFIER_REGEX = /^[a-zA-Z0-9_]+$/;
|
||||
|
||||
/** Validates base arguments required for identifying boolean switch data and columns. */
|
||||
export function validateBaseBooleanArgs(args: BooleanSwitchDataArgs): void {
|
||||
if (!args.table || !args.habitKey || !args.date) {
|
||||
throw new Error(`Missing required identifying arguments: table='${args.table}', key='${args.habitKey}', date='${args.date}'`);
|
||||
}
|
||||
if (!args.habitIdCol || !IDENTIFIER_REGEX.test(args.habitIdCol)) {
|
||||
throw new Error(`Invalid or missing key ID column name: '${args.habitIdCol}'`);
|
||||
}
|
||||
if (!args.valueCol || !IDENTIFIER_REGEX.test(args.valueCol)) {
|
||||
throw new Error(`Invalid or missing value column name: '${args.valueCol}'`);
|
||||
}
|
||||
if (!args.dateCol || !IDENTIFIER_REGEX.test(args.dateCol)) {
|
||||
throw new Error(`Invalid or missing date column name: '${args.dateCol}'`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Validates arguments required for upserting boolean switch data. */
|
||||
export function validateUpsertBooleanArgs(args: UpsertBooleanSwitchArgs): void {
|
||||
validateBaseBooleanArgs(args); // Includes conditional uuidCol validation
|
||||
if (args.newValue !== 0 && args.newValue !== 1) { //^ Check for 0 or 1
|
||||
throw new Error(`Invalid newValue for boolean upsert: '${args.newValue}' (must be 0 or 1)`);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue