diff --git a/src/ui/basedClasses/BaseSettingComponent.ts b/src/ui/basedClasses/BaseSettingComponent.ts index bdeed80..47a2341 100644 --- a/src/ui/basedClasses/BaseSettingComponent.ts +++ b/src/ui/basedClasses/BaseSettingComponent.ts @@ -25,4 +25,16 @@ export abstract class BaseSettingComponent implements ISettingComponent { public setChangeListener(listener: () => void): void { this.onChangeCallback = listener; } -} \ No newline at end of file + + /** + * Creates a DocumentFragment from an HTML string. + * Useful for setting complex descriptions with HTML content. + * @param html The HTML string. + * @returns A DocumentFragment containing the parsed HTML. + */ + protected createFragmentWithHTML(html: string): DocumentFragment { + return createFragment((documentFragment) => { + documentFragment.createDiv().innerHTML = html; + }); + } +} diff --git a/src/ui/components/pathGroup/PathGlobsSettingComponent.ts b/src/ui/components/pathGroup/PathGlobsSettingComponent.ts index 0dbbf7b..3c317f1 100644 --- a/src/ui/components/pathGroup/PathGlobsSettingComponent.ts +++ b/src/ui/components/pathGroup/PathGlobsSettingComponent.ts @@ -3,89 +3,130 @@ import { CheckboxSyncPluginSettings } from 'src/types'; import { BaseSettingComponent } from 'src/ui/basedClasses/BaseSettingComponent'; import { ValidationError } from 'src/ui/validation/types'; + export class PathGlobsSettingComponent extends BaseSettingComponent { - private textInput: TextAreaComponent; + private textInput: TextAreaComponent; - getSettingKey(): keyof CheckboxSyncPluginSettings { - return 'pathGlobs'; - } + getSettingKey(): keyof CheckboxSyncPluginSettings { + return 'pathGlobs'; + } - private arrayToMultilineString(value: string[] | undefined | null): string { - if (Array.isArray(value)) { - try { - return value.join('\n'); - } catch (e) { - console.error(`[${this.getSettingKey()}] Error joining array to multiline string: `, e); - return ''; - } - } - return ''; - } + private arrayToMultilineString(value: string[] | undefined | null): string { + if (Array.isArray(value)) { + try { + return value.join('\n'); + } catch (e) { + console.error(`[${this.getSettingKey()}] Error joining array to multiline string: `, e); + return ''; + } + } + return ''; + } - render(container: HTMLElement, currentValue: any): void { - const multilineStringValue = this.arrayToMultilineString(currentValue as string[] | undefined); + render(container: HTMLElement, currentValue: any): void { + const multilineStringValue = this.arrayToMultilineString(currentValue as string[] | undefined); - this.setting = new Setting(container) - .setName('Ignored Files & Folders') - .setDesc('List files or folders that the plugin should IGNORE. ' + - 'This uses "glob patterns" (similar to .gitignore syntax) to match paths.\n' + - '- Each pattern on a new line. If this list is empty, no files are ignored (plugin processes all).\n' + - '- Example to IGNORE: `My Folder/` or `*.log`\n' + - '- Example to PROCESS an item within an IGNORED path: `!My Folder/My Important File.md`\n' + - '- The last rule that matches a file determines its fate.' - ) - .addTextArea(text => { - this.textInput = text; - text.setPlaceholder( - '# Lines starting with # are comments and will be ignored\n' + - 'Drafts/\n' + - '*.tmp\n' + - '!Drafts/ReadyToPublish.md\n\n' + - '# To process ONLY files in "Projects" folder:\n' + - '*\n' + - '!Projects/**' - ); - text.inputEl.setAttr('rows', 10); // Увеличил немного для плейсхолдера - text.inputEl.style.width = '100%'; - - text.setValue(multilineStringValue); - - text.onChange(() => { - this.onChangeCallback(); - }); - }); - } + const descHtml = + `

List files or folders that the plugin should IGNORE. + This uses "glob patterns" (similar to .gitignore syntax) to match paths.

+ `; - getValueFromUi(): string[] { - if (this.textInput) { - const rawValue = this.textInput.getValue(); - return rawValue - .split('\n') - .map(s => s.trim()) - .filter(s => s.length > 0 && !s.startsWith('#')); // Игнорируем пустые строки и комментарии - } - throw new Error(`[${this.getSettingKey()}] Cannot get value from UI: TextArea component not rendered or available.`); - } + this.setting = new Setting(container) + .setName('Ignored Files & Folders') + // Используем createFragmentWithHTML для описания + .setDesc(this.createFragmentWithHTML(descHtml)) + .addTextArea(text => { + this.textInput = text; + text.setPlaceholder( + '# Lines starting with # are comments and will be ignored\n' + + 'Drafts/\n' + + '*.tmp\n' + + '!Drafts/ReadyToPublish.md\n\n' + + '# To process ONLY files in "Projects" folder:\n' + + '*\n' + + '!Projects/**' + ); - setValueInUi(value: any): void { - if (this.textInput) { - if (value === undefined) { - this.textInput.setValue(''); - return; - } - // Ожидаем, что value это string[]. Если нет, будет ошибка времени выполнения при .join() - this.textInput.setValue((value as string[]).join('\n')); - } else { - throw new Error(`[${this.getSettingKey()}] Cannot set value in UI: TextArea component not rendered or available.`); - } - } + text.setValue(multilineStringValue); - validate(): ValidationError | null { - try { - this.getValueFromUi(); - } catch (e: any) { - return { field: this.getSettingKey(), message: e.message }; - } - return null; - } + text.onChange(() => { + this.onChangeCallback(); + }); + }); + + // Применяем стиль "многострочной настройки" как в Tasks плагине + this.makeMultilineTextSetting(); + } + + /** + * Изменяет стиль Setting, чтобы описание было над TextArea, а TextArea занимала всю ширину. + */ + private makeMultilineTextSetting(): void { + if (!this.setting) return; + + const { settingEl, infoEl, controlEl } = this.setting; + const textAreaEl: HTMLTextAreaElement | null = controlEl.querySelector('textarea'); + + if (textAreaEl === null) { + // Это не настройка с TextArea, ничего не делаем + return; + } + + settingEl.style.display = 'block'; + // infoEl может не существовать, если .setName() и .setDesc() не вызывались или были очищены. + // В нашем случае они вызываются, так что infoEl должен быть. + if (infoEl) { + infoEl.style.marginBottom = 'var(--size-4-2)'; // Небольшой отступ под описанием + infoEl.style.marginRight = '0px'; // Убираем отступ справа, если он был + } + + // Для controlEl (контейнер для textarea) + controlEl.style.width = '100%'; // Заставляем контейнер контрола быть на всю ширину + + // Для самой textarea + textAreaEl.style.width = '100%'; // Занимает всю ширину controlEl + textAreaEl.style.minHeight = '120px'; // Минимальная высота для нескольких строк + textAreaEl.rows = 8; + } + + + getValueFromUi(): string[] { + if (this.textInput) { + const rawValue = this.textInput.getValue(); + return rawValue + .split('\n') + .map(s => s.trim()) + .filter(s => s.length > 0 && !s.startsWith('#')); + } + throw new Error(`[${this.getSettingKey()}] Cannot get value from UI: TextArea component not rendered or available.`); + } + + setValueInUi(value: any): void { + if (this.textInput) { + if (value === undefined) { + this.textInput.setValue(''); + return; + } + this.textInput.setValue((value as string[]).join('\n')); + } else { + throw new Error(`[${this.getSettingKey()}] Cannot set value in UI: TextArea component not rendered or available.`); + } + } + + validate(): ValidationError | null { + try { + this.getValueFromUi(); + } catch (e: any) { + return { field: this.getSettingKey(), message: e.message }; + } + return null; + } }