diff --git a/src/ui/SettingGroup.ts b/src/ui/SettingGroup.ts new file mode 100644 index 0000000..4677159 --- /dev/null +++ b/src/ui/SettingGroup.ts @@ -0,0 +1,56 @@ +import { CheckboxSyncPluginSettings } from "src/types"; +import { ISettingComponent } from "./interfaces/ISettingComponent"; + +/** + * Представляет группу связанных настроек в UI. + */ +export class SettingGroup { + title: string; + description?: string; + components: ISettingComponent[]; + + /** + * Создает экземпляр группы настроек. + * @param title - Заголовок группы (отображается как h3). + * @param components - Массив компонентов настроек, входящих в эту группу. + * @param description - Опциональное описание группы (отображается как p). + */ + constructor(title: string, components: ISettingComponent[], description?: string) { + this.title = title; + this.components = components; + this.description = description; + } + + /** + * Рендерит заголовок группы и все ее компоненты. + * @param container - HTML-элемент, куда рендерить группу. + * @param currentSettings - Объект текущих настроек плагина. + */ + render(container: HTMLElement, currentSettings: CheckboxSyncPluginSettings): void { + container.createEl('h3', { text: this.title }); + if (this.description) { + container.createEl('p', { text: this.description, cls: 'setting-item-description' }); + } + + // Рендерим каждый компонент, передавая ему текущее значение + for (const component of this.components) { + try { + const key = component.getSettingKey(); + // Проверяем, существует ли ключ в настройках, чтобы избежать ошибок + if (key in currentSettings) { + component.render(container, currentSettings[key]); + } else { + console.warn(`Setting key "${key}" not found in current settings for component in group "${this.title}". Rendering with default or undefined value might occur.`); + // Можно передать undefined или значение по умолчанию, если getSettingKey гарантированно работает + // component.render(container, component.getDefaultValue()); + component.render(container, undefined); // Или рендерить с undefined + } + + } catch (error) { + console.error(`Error rendering component for key "${component.getSettingKey()}" in group "${this.title}":`, error); + // Можно добавить placeholder или сообщение об ошибке в UI + container.createDiv({ text: `Error rendering setting: ${component.getSettingKey()}`, cls: 'checkbox-sync-settings-error' }); + } + } + } +} \ No newline at end of file diff --git a/src/ui/components/BaseSettingComponent.ts b/src/ui/components/BaseSettingComponent.ts new file mode 100644 index 0000000..fb319ad --- /dev/null +++ b/src/ui/components/BaseSettingComponent.ts @@ -0,0 +1,27 @@ +import { Setting } from 'obsidian'; +import { CheckboxSyncPluginSettings } from '../../types'; +import { ISettingComponent } from '../interfaces/ISettingComponent'; +import { ValidationError } from '../validation/types'; +// Убрали App и CheckboxSyncPlugin из импортов, если они не нужны ВСЕМ наследникам + +export abstract class BaseSettingComponent implements ISettingComponent { + protected setting: Setting; // Инициализируется в render конкретного компонента + protected onChangeCallback: () => void = () => {}; + + // Конструктор теперь пустой или с минимальными общими зависимостями + constructor() {} + + // --- Методы для реализации наследниками --- + abstract getSettingKey(): keyof CheckboxSyncPluginSettings; + abstract getDefaultValue(): any; + // render теперь должен сам создавать Setting и сохранять ссылку в this.setting + abstract render(container: HTMLElement, currentValue: any): void; + abstract getValueFromUi(): any; + abstract setValueInUi(value: any): void; + abstract validate(value: any): ValidationError | null; + + // --- Общая реализация --- + public setChangeListener(listener: () => void): void { + this.onChangeCallback = listener; + } +} \ No newline at end of file diff --git a/src/ui/interfaces/ISettingComponent.ts b/src/ui/interfaces/ISettingComponent.ts new file mode 100644 index 0000000..15ab5a4 --- /dev/null +++ b/src/ui/interfaces/ISettingComponent.ts @@ -0,0 +1,58 @@ +import { CheckboxSyncPluginSettings } from "src/types"; +import { ValidationError } from "../validation/types"; + + +/** + * Интерфейс для компонента, отвечающего за одну настройку в UI. + */ +export interface ISettingComponent { + /** + * Получает ключ настройки, за которую отвечает этот компонент. + * @returns Ключ из CheckboxSyncPluginSettings. + */ + getSettingKey(): keyof CheckboxSyncPluginSettings; + + /** + * Получает значение по умолчанию для этой настройки. + * @returns Значение по умолчанию. + */ + getDefaultValue(): any; + + /** + * Рендерит UI-элементы для этой настройки в указанный контейнер. + * @param container - HTML-элемент, куда добавлять настройку. + * @param currentValue - Текущее сохраненное значение настройки. + */ + render(container: HTMLElement, currentValue: any): void; + + /** + * Считывает значение из UI-элементов компонента. + * Может включать парсинг (например, JSON). + * @returns Текущее значение из UI. + * @throws Error если значение в UI некорректно для извлечения (например, невалидный JSON). + */ + getValueFromUi(): any; + + /** + * Устанавливает значение в UI-элементы компонента. + * Может включать форматирование (например, JSON.stringify). + * @param value - Значение для установки в UI. + */ + setValueInUi(value: any): void; + + /** + * Выполняет *индивидуальную* валидацию значения. + * **Важно:** Этот метод должен быть тестируемым и не зависеть от Obsidian API. + * Он принимает чистое значение и возвращает ошибку или null. + * @param value - Чистое значение для валидации (обычно результат getValueFromUi). + * @returns Объект ValidationError, если значение невалидно, иначе null. + */ + validate(value: any): ValidationError | null; + + /** + * Устанавливает колбэк, который будет вызван при изменении значения в UI. + * Используется для управления флагом isDirty. + * @param listener - Функция обратного вызова. + */ + setChangeListener(listener: () => void): void; +} \ No newline at end of file diff --git a/src/ui/validation/SettingsValidator.ts b/src/ui/validation/SettingsValidator.ts new file mode 100644 index 0000000..e7e9b54 --- /dev/null +++ b/src/ui/validation/SettingsValidator.ts @@ -0,0 +1,40 @@ +// src/settings/validation/SettingsValidator.ts + +import { CheckboxSyncPluginSettings } from '../../types'; // Путь к типам +import { ValidationError } from './types'; // Путь к типам ошибок + +/** + * Отвечает за валидацию настроек, требующую проверки нескольких полей (перекрестная валидация). + * **Важно:** Логика внутри методов не должна зависеть от Obsidian API для тестируемости. + */ +export class SettingsValidator { + + /** + * Выполняет перекрестную валидацию предоставленных данных настроек. + * @param settingsData - Объект с текущими значениями настроек (или их частью). + * @returns Массив объектов ValidationError, если найдены ошибки, иначе пустой массив. + */ + public validate(settingsData: Partial): ValidationError[] { + const errors: ValidationError[] = []; + + // Пример: Проверка пересечения списков символов (будет реализована позже) + // if (settingsData.checkedSymbols && settingsData.uncheckedSymbols) { + // const intersection = settingsData.checkedSymbols.filter(symbol => settingsData.uncheckedSymbols!.includes(symbol)); + // if (intersection.length > 0) { + // errors.push({ + // // Можно не указывать поле, т.к. ошибка затрагивает несколько + // message: `Symbols found in both Checked and Unchecked lists: ${JSON.stringify(intersection)}` + // }); + // } + // } + // ... другие перекрестные проверки ... + + // Пока возвращаем пустой массив, логика будет добавлена на Шаге 6 + return errors; + } + + // Сюда можно добавить статические вспомогательные методы для валидации, если нужно + // public static validateSymbolListsIntersection(listA: string[], listB: string[]): string[] { + // return listA.filter(symbol => listB.includes(symbol)); + // } +} \ No newline at end of file diff --git a/src/ui/validation/types.ts b/src/ui/validation/types.ts new file mode 100644 index 0000000..29b74eb --- /dev/null +++ b/src/ui/validation/types.ts @@ -0,0 +1,8 @@ +import { CheckboxSyncPluginSettings } from "src/types"; + +export interface ValidationError { + /** Ключ поля настройки, с которым связана ошибка (опционально). */ + field?: keyof CheckboxSyncPluginSettings; + /** Сообщение об ошибке, понятное пользователю. */ + message: string; +} \ No newline at end of file