@real1ty-obsidian-plugins/utils 2.2.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +29 -9
  2. package/dist/{async-utils.d.ts → async/async.d.ts} +1 -1
  3. package/dist/async/async.d.ts.map +1 -0
  4. package/dist/{async-utils.js → async/async.js} +1 -1
  5. package/dist/async/async.js.map +1 -0
  6. package/dist/async/batch-operations.d.ts.map +1 -0
  7. package/dist/async/batch-operations.js.map +1 -0
  8. package/dist/async/index.d.ts +3 -0
  9. package/dist/async/index.d.ts.map +1 -0
  10. package/dist/async/index.js +3 -0
  11. package/dist/async/index.js.map +1 -0
  12. package/dist/core/evaluator-base.d.ts.map +1 -0
  13. package/dist/core/evaluator-base.js.map +1 -0
  14. package/dist/core/generate.d.ts.map +1 -0
  15. package/dist/core/generate.js.map +1 -0
  16. package/dist/core/index.d.ts +3 -0
  17. package/dist/core/index.d.ts.map +1 -0
  18. package/dist/core/index.js +3 -0
  19. package/dist/core/index.js.map +1 -0
  20. package/dist/{date-recurrence-utils.d.ts → date/date-recurrence.d.ts} +1 -1
  21. package/dist/date/date-recurrence.d.ts.map +1 -0
  22. package/dist/{date-recurrence-utils.js → date/date-recurrence.js} +1 -1
  23. package/dist/date/date-recurrence.js.map +1 -0
  24. package/dist/{date-utils.d.ts → date/date.d.ts} +1 -1
  25. package/dist/date/date.d.ts.map +1 -0
  26. package/dist/{date-utils.js → date/date.js} +1 -1
  27. package/dist/date/date.js.map +1 -0
  28. package/dist/date/index.d.ts +3 -0
  29. package/dist/date/index.d.ts.map +1 -0
  30. package/dist/date/index.js +3 -0
  31. package/dist/date/index.js.map +1 -0
  32. package/dist/{child-reference-utils.d.ts → file/child-reference.d.ts} +1 -1
  33. package/dist/file/child-reference.d.ts.map +1 -0
  34. package/dist/{child-reference-utils.js → file/child-reference.js} +1 -1
  35. package/dist/file/child-reference.js.map +1 -0
  36. package/dist/file/file-operations.d.ts.map +1 -0
  37. package/dist/{file-operations.js → file/file-operations.js} +2 -2
  38. package/dist/file/file-operations.js.map +1 -0
  39. package/dist/file/file.d.ts +263 -0
  40. package/dist/file/file.d.ts.map +1 -0
  41. package/dist/file/file.js +466 -0
  42. package/dist/file/file.js.map +1 -0
  43. package/dist/{frontmatter-utils.d.ts → file/frontmatter.d.ts} +1 -1
  44. package/dist/file/frontmatter.d.ts.map +1 -0
  45. package/dist/{frontmatter-utils.js → file/frontmatter.js} +1 -1
  46. package/dist/file/frontmatter.js.map +1 -0
  47. package/dist/file/index.d.ts +7 -0
  48. package/dist/file/index.d.ts.map +1 -0
  49. package/dist/file/index.js +7 -0
  50. package/dist/file/index.js.map +1 -0
  51. package/dist/file/link-parser.d.ts.map +1 -0
  52. package/dist/file/link-parser.js.map +1 -0
  53. package/dist/{templater-utils.d.ts → file/templater.d.ts} +1 -1
  54. package/dist/file/templater.d.ts.map +1 -0
  55. package/dist/{templater-utils.js → file/templater.js} +1 -1
  56. package/dist/file/templater.js.map +1 -0
  57. package/dist/index.d.ts +6 -15
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +12 -15
  60. package/dist/index.js.map +1 -1
  61. package/dist/settings/index.d.ts +3 -0
  62. package/dist/settings/index.d.ts.map +1 -0
  63. package/dist/settings/index.js +3 -0
  64. package/dist/settings/index.js.map +1 -0
  65. package/dist/settings/settings-store.d.ts.map +1 -0
  66. package/dist/settings/settings-store.js.map +1 -0
  67. package/dist/settings/settings-ui-builder.d.ts.map +1 -0
  68. package/dist/settings/settings-ui-builder.js.map +1 -0
  69. package/dist/string/index.d.ts +2 -0
  70. package/dist/string/index.d.ts.map +1 -0
  71. package/dist/string/index.js +2 -0
  72. package/dist/string/index.js.map +1 -0
  73. package/dist/{string-utils.d.ts → string/string.d.ts} +1 -1
  74. package/dist/string/string.d.ts.map +1 -0
  75. package/dist/{string-utils.js → string/string.js} +1 -1
  76. package/dist/string/string.js.map +1 -0
  77. package/dist/testing/mocks/obsidian.d.ts +1 -0
  78. package/dist/testing/mocks/obsidian.d.ts.map +1 -1
  79. package/dist/testing/mocks/obsidian.js +6 -0
  80. package/dist/testing/mocks/obsidian.js.map +1 -1
  81. package/package.json +3 -5
  82. package/src/async/async.ts +117 -0
  83. package/src/async/batch-operations.ts +53 -0
  84. package/src/async/index.ts +2 -0
  85. package/src/core/evaluator-base.ts +118 -0
  86. package/src/core/generate.ts +22 -0
  87. package/src/core/index.ts +2 -0
  88. package/src/date/date-recurrence.ts +244 -0
  89. package/src/date/date.ts +111 -0
  90. package/src/date/index.ts +2 -0
  91. package/src/file/child-reference.ts +76 -0
  92. package/src/file/file-operations.ts +197 -0
  93. package/src/file/file.ts +570 -0
  94. package/src/file/frontmatter.ts +80 -0
  95. package/src/file/index.ts +6 -0
  96. package/src/file/link-parser.ts +18 -0
  97. package/src/file/templater.ts +75 -0
  98. package/src/index.ts +14 -0
  99. package/src/settings/index.ts +2 -0
  100. package/src/settings/settings-store.ts +88 -0
  101. package/src/settings/settings-ui-builder.ts +507 -0
  102. package/src/string/index.ts +1 -0
  103. package/src/string/string.ts +26 -0
  104. package/src/testing/index.ts +23 -0
  105. package/src/testing/mocks/obsidian.ts +331 -0
  106. package/src/testing/mocks/utils.ts +113 -0
  107. package/src/testing/setup.ts +19 -0
  108. package/dist/async-utils.d.ts.map +0 -1
  109. package/dist/async-utils.js.map +0 -1
  110. package/dist/batch-operations.d.ts.map +0 -1
  111. package/dist/batch-operations.js.map +0 -1
  112. package/dist/child-reference-utils.d.ts.map +0 -1
  113. package/dist/child-reference-utils.js.map +0 -1
  114. package/dist/date-recurrence-utils.d.ts.map +0 -1
  115. package/dist/date-recurrence-utils.js.map +0 -1
  116. package/dist/date-utils.d.ts.map +0 -1
  117. package/dist/date-utils.js.map +0 -1
  118. package/dist/evaluator-base.d.ts.map +0 -1
  119. package/dist/evaluator-base.js.map +0 -1
  120. package/dist/file-operations.d.ts.map +0 -1
  121. package/dist/file-operations.js.map +0 -1
  122. package/dist/file-utils.d.ts +0 -6
  123. package/dist/file-utils.d.ts.map +0 -1
  124. package/dist/file-utils.js +0 -25
  125. package/dist/file-utils.js.map +0 -1
  126. package/dist/frontmatter-utils.d.ts.map +0 -1
  127. package/dist/frontmatter-utils.js.map +0 -1
  128. package/dist/generate.d.ts.map +0 -1
  129. package/dist/generate.js.map +0 -1
  130. package/dist/link-parser.d.ts.map +0 -1
  131. package/dist/link-parser.js.map +0 -1
  132. package/dist/settings-store.d.ts.map +0 -1
  133. package/dist/settings-store.js.map +0 -1
  134. package/dist/settings-ui-builder.d.ts.map +0 -1
  135. package/dist/settings-ui-builder.js.map +0 -1
  136. package/dist/string-utils.d.ts.map +0 -1
  137. package/dist/string-utils.js.map +0 -1
  138. package/dist/templater-utils.d.ts.map +0 -1
  139. package/dist/templater-utils.js.map +0 -1
  140. /package/dist/{batch-operations.d.ts → async/batch-operations.d.ts} +0 -0
  141. /package/dist/{batch-operations.js → async/batch-operations.js} +0 -0
  142. /package/dist/{evaluator-base.d.ts → core/evaluator-base.d.ts} +0 -0
  143. /package/dist/{evaluator-base.js → core/evaluator-base.js} +0 -0
  144. /package/dist/{generate.d.ts → core/generate.d.ts} +0 -0
  145. /package/dist/{generate.js → core/generate.js} +0 -0
  146. /package/dist/{file-operations.d.ts → file/file-operations.d.ts} +0 -0
  147. /package/dist/{link-parser.d.ts → file/link-parser.d.ts} +0 -0
  148. /package/dist/{link-parser.js → file/link-parser.js} +0 -0
  149. /package/dist/{settings-store.d.ts → settings/settings-store.d.ts} +0 -0
  150. /package/dist/{settings-store.js → settings/settings-store.js} +0 -0
  151. /package/dist/{settings-ui-builder.d.ts → settings/settings-ui-builder.d.ts} +0 -0
  152. /package/dist/{settings-ui-builder.js → settings/settings-ui-builder.js} +0 -0
@@ -0,0 +1,75 @@
1
+ import { type App, Notice, normalizePath, type TFile } from "obsidian";
2
+
3
+ const TEMPLATER_ID = "templater-obsidian";
4
+
5
+ type CreateFn = (
6
+ templateFile: TFile,
7
+ folder?: string,
8
+ filename?: string,
9
+ openNewNote?: boolean
10
+ ) => Promise<TFile | undefined>;
11
+
12
+ interface TemplaterLike {
13
+ create_new_note_from_template: CreateFn;
14
+ }
15
+
16
+ async function waitForTemplater(app: App, timeoutMs = 8000): Promise<TemplaterLike | null> {
17
+ await new Promise<void>((resolve) => app.workspace.onLayoutReady(resolve));
18
+
19
+ const started = Date.now();
20
+ while (Date.now() - started < timeoutMs) {
21
+ const plug: any = (app as any).plugins?.getPlugin?.(TEMPLATER_ID);
22
+ const api = plug?.templater ?? null;
23
+
24
+ const createFn: CreateFn | undefined = api?.create_new_note_from_template?.bind(api);
25
+ if (typeof createFn === "function") {
26
+ return { create_new_note_from_template: createFn };
27
+ }
28
+ await new Promise((r) => setTimeout(r, 150));
29
+ }
30
+ return null;
31
+ }
32
+
33
+ export function isTemplaterAvailable(app: App): boolean {
34
+ const instance = (app as any).plugins?.getPlugin?.(TEMPLATER_ID);
35
+ return !!instance;
36
+ }
37
+
38
+ export async function createFromTemplate(
39
+ app: App,
40
+ templatePath: string,
41
+ targetFolder?: string,
42
+ filename?: string,
43
+ openNewNote = false
44
+ ): Promise<TFile | null> {
45
+ const templater = await waitForTemplater(app);
46
+ if (!templater) {
47
+ console.warn("Templater isn't ready yet (or not installed/enabled).");
48
+ new Notice(
49
+ "Templater plugin is not available or enabled. Please ensure it is installed and enabled."
50
+ );
51
+ return null;
52
+ }
53
+
54
+ const templateFile = app.vault.getFileByPath(normalizePath(templatePath));
55
+ if (!templateFile) {
56
+ console.error(`Template not found: ${templatePath}`);
57
+ new Notice(`Template file not found: ${templatePath}. Please ensure the template file exists.`);
58
+ return null;
59
+ }
60
+
61
+ try {
62
+ const newFile = await templater.create_new_note_from_template(
63
+ templateFile,
64
+ targetFolder,
65
+ filename,
66
+ openNewNote
67
+ );
68
+
69
+ return newFile ?? null;
70
+ } catch (error) {
71
+ console.error("Error creating file from template:", error);
72
+ new Notice("Error creating file from template. Please ensure the template file is valid.");
73
+ return null;
74
+ }
75
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Settings
2
+
3
+ // Async utilities
4
+ export * from "./async";
5
+ // Core utilities
6
+ export * from "./core";
7
+
8
+ // Date operations
9
+ export * from "./date";
10
+ // File operations
11
+ export * from "./file";
12
+ export * from "./settings";
13
+ // String utilities
14
+ export * from "./string";
@@ -0,0 +1,2 @@
1
+ export * from "./settings-store";
2
+ export * from "./settings-ui-builder";
@@ -0,0 +1,88 @@
1
+ import type { Plugin } from "obsidian";
2
+ import { BehaviorSubject } from "rxjs";
3
+ import type { z } from "zod";
4
+
5
+ export class SettingsStore<TSchema extends z.ZodTypeAny> {
6
+ private plugin: Plugin;
7
+ private schema: TSchema;
8
+ public readonly settings$: BehaviorSubject<z.infer<TSchema>>;
9
+
10
+ constructor(plugin: Plugin, schema: TSchema) {
11
+ this.plugin = plugin;
12
+ this.schema = schema;
13
+ this.settings$ = new BehaviorSubject<z.infer<TSchema>>(schema.parse({}));
14
+ }
15
+
16
+ get currentSettings(): z.infer<TSchema> {
17
+ return this.settings$.value;
18
+ }
19
+
20
+ get validationSchema(): TSchema {
21
+ return this.schema;
22
+ }
23
+
24
+ async loadSettings(): Promise<void> {
25
+ try {
26
+ const data = await this.plugin.loadData();
27
+ const sanitized = this.schema.parse(data ?? {});
28
+ this.settings$.next(sanitized);
29
+
30
+ // Save back if data was sanitized/normalized
31
+ if (JSON.stringify(sanitized) !== JSON.stringify(data ?? {})) {
32
+ await this.saveSettings();
33
+ }
34
+ } catch (error) {
35
+ console.error("Failed to load settings, using defaults:", error);
36
+ this.settings$.next(this.schema.parse({}));
37
+ await this.saveSettings();
38
+ }
39
+ }
40
+
41
+ async saveSettings(): Promise<void> {
42
+ await this.plugin.saveData(this.currentSettings);
43
+ }
44
+
45
+ async updateSettings(updater: (settings: z.infer<TSchema>) => z.infer<TSchema>): Promise<void> {
46
+ try {
47
+ const newSettings = updater(this.currentSettings);
48
+ const validated = this.schema.parse(newSettings);
49
+
50
+ this.settings$.next(validated);
51
+ await this.saveSettings();
52
+ } catch (error) {
53
+ console.error("Failed to update settings:", error);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async resetSettings(): Promise<void> {
59
+ this.settings$.next(this.schema.parse({}));
60
+ await this.saveSettings();
61
+ }
62
+
63
+ async updateProperty<K extends keyof z.infer<TSchema>>(
64
+ key: K,
65
+ value: z.infer<TSchema>[K]
66
+ ): Promise<void> {
67
+ await this.updateSettings((settings) => {
68
+ const updated = Object.assign({}, settings);
69
+ (updated as any)[key] = value;
70
+ return updated;
71
+ });
72
+ }
73
+
74
+ async updateProperties(updates: Partial<z.infer<TSchema>>): Promise<void> {
75
+ await this.updateSettings((settings) => {
76
+ return Object.assign({}, settings, updates);
77
+ });
78
+ }
79
+
80
+ getDefaults(): z.infer<TSchema> {
81
+ return this.schema.parse({});
82
+ }
83
+
84
+ hasCustomizations(): boolean {
85
+ const defaults = this.getDefaults();
86
+ return JSON.stringify(this.currentSettings) !== JSON.stringify(defaults);
87
+ }
88
+ }
@@ -0,0 +1,507 @@
1
+ import { Notice, Setting } from "obsidian";
2
+ import type { ZodArray, ZodNumber, ZodObject, ZodRawShape, z } from "zod";
3
+ import type { SettingsStore } from "./settings-store";
4
+
5
+ interface BaseSettingConfig {
6
+ key: string;
7
+ name: string;
8
+ desc: string;
9
+ }
10
+
11
+ interface TextSettingConfig extends BaseSettingConfig {
12
+ placeholder?: string;
13
+ commitOnChange?: boolean;
14
+ }
15
+
16
+ interface SliderSettingConfig extends BaseSettingConfig {
17
+ min?: number;
18
+ max?: number;
19
+ step?: number;
20
+ commitOnChange?: boolean;
21
+ }
22
+
23
+ interface DropdownSettingConfig extends BaseSettingConfig {
24
+ options: Record<string, string>;
25
+ }
26
+
27
+ interface ArraySettingConfig<T = string> extends BaseSettingConfig {
28
+ placeholder?: string;
29
+ arrayDelimiter?: string;
30
+ multiline?: boolean;
31
+ commitOnChange?: boolean;
32
+ itemType?: "string" | "number";
33
+ parser?: (input: string) => T;
34
+ validator?: (item: T) => boolean;
35
+ }
36
+
37
+ interface ArrayManagerConfig extends BaseSettingConfig {
38
+ placeholder?: string;
39
+ addButtonText?: string;
40
+ removeButtonText?: string;
41
+ emptyArrayFallback?: unknown;
42
+ preventEmpty?: boolean;
43
+ itemDescriptionFn?: (item: unknown) => string;
44
+ onBeforeAdd?: (newItem: unknown, currentItems: unknown[]) => unknown[] | Promise<unknown[]>;
45
+ onBeforeRemove?: (
46
+ itemToRemove: unknown,
47
+ currentItems: unknown[]
48
+ ) => unknown[] | Promise<unknown[]>;
49
+ quickActions?: Array<{
50
+ name: string;
51
+ desc: string;
52
+ buttonText: string;
53
+ condition?: (currentItems: unknown[]) => boolean;
54
+ action: (currentItems: unknown[]) => unknown[] | Promise<unknown[]>;
55
+ }>;
56
+ }
57
+
58
+ export class SettingsUIBuilder<TSchema extends ZodObject<ZodRawShape>> {
59
+ constructor(private settingsStore: SettingsStore<TSchema>) {}
60
+
61
+ private get settings(): z.infer<TSchema> {
62
+ return this.settingsStore.currentSettings;
63
+ }
64
+
65
+ private get schema(): TSchema {
66
+ return this.settingsStore.validationSchema;
67
+ }
68
+
69
+ private async updateSetting(key: keyof z.infer<TSchema>, value: unknown): Promise<void> {
70
+ const newSettings = {
71
+ ...this.settings,
72
+ [key]: value,
73
+ } as z.infer<TSchema>;
74
+
75
+ const result = this.schema.safeParse(newSettings);
76
+
77
+ if (!result.success) {
78
+ const errors = result.error.issues
79
+ .map((e) => `${String(e.path.join("."))}${e.path.length > 0 ? ": " : ""}${e.message}`)
80
+ .join(", ");
81
+ new Notice(`Validation failed: ${errors}`, 5000);
82
+ throw new Error(`Validation failed: ${errors}`);
83
+ }
84
+
85
+ await this.settingsStore.updateSettings(() => newSettings);
86
+ }
87
+
88
+ private inferSliderBounds(key: string): { min?: number; max?: number; step?: number } {
89
+ try {
90
+ const fieldSchema = (this.schema.shape as any)[key];
91
+ if (!fieldSchema) return {};
92
+
93
+ let innerSchema = fieldSchema;
94
+ while ((innerSchema as any)._def?.innerType) {
95
+ innerSchema = (innerSchema as any)._def.innerType;
96
+ }
97
+
98
+ if ((innerSchema as any)._def?.typeName === "ZodNumber") {
99
+ const checks = ((innerSchema as ZodNumber)._def as any).checks || [];
100
+ let min: number | undefined;
101
+ let max: number | undefined;
102
+
103
+ for (const check of checks) {
104
+ if ((check as any).kind === "min") {
105
+ min = (check as any).value;
106
+ }
107
+ if ((check as any).kind === "max") {
108
+ max = (check as any).value;
109
+ }
110
+ }
111
+
112
+ return { min, max };
113
+ }
114
+ } catch (error) {
115
+ console.warn(`Failed to infer slider bounds for key ${key}:`, error);
116
+ }
117
+
118
+ return {};
119
+ }
120
+
121
+ private inferArrayItemType(key: string): "string" | "number" | undefined {
122
+ try {
123
+ const fieldSchema = (this.schema.shape as any)[key];
124
+ if (!fieldSchema) return undefined;
125
+
126
+ let innerSchema = fieldSchema;
127
+ while ((innerSchema as any)._def?.innerType) {
128
+ innerSchema = (innerSchema as any)._def.innerType;
129
+ }
130
+
131
+ if ((innerSchema as any)._def?.typeName === "ZodArray") {
132
+ const elementType = ((innerSchema as ZodArray<any>)._def as any).type;
133
+ if ((elementType as any)._def?.typeName === "ZodNumber") {
134
+ return "number";
135
+ }
136
+ if ((elementType as any)._def?.typeName === "ZodString") {
137
+ return "string";
138
+ }
139
+ }
140
+ } catch (error) {
141
+ console.warn(`Failed to infer array item type for key ${key}:`, error);
142
+ }
143
+
144
+ return undefined;
145
+ }
146
+
147
+ addToggle(containerEl: HTMLElement, config: BaseSettingConfig): void {
148
+ const { key, name, desc } = config;
149
+ const value = this.settings[key as keyof z.infer<TSchema>];
150
+
151
+ new Setting(containerEl)
152
+ .setName(name)
153
+ .setDesc(desc)
154
+ .addToggle((toggle) =>
155
+ toggle.setValue(Boolean(value)).onChange(async (newValue) => {
156
+ await this.updateSetting(key as keyof z.infer<TSchema>, newValue);
157
+ })
158
+ );
159
+ }
160
+
161
+ addSlider(containerEl: HTMLElement, config: SliderSettingConfig): void {
162
+ const { key, name, desc, step = 1, commitOnChange = false } = config;
163
+ const value = this.settings[key as keyof z.infer<TSchema>];
164
+
165
+ const inferredBounds = this.inferSliderBounds(key);
166
+ const min = config.min ?? inferredBounds.min ?? 0;
167
+ const max = config.max ?? inferredBounds.max ?? 100;
168
+
169
+ new Setting(containerEl)
170
+ .setName(name)
171
+ .setDesc(desc)
172
+ .addSlider((slider) => {
173
+ slider.setLimits(min, max, step).setValue(Number(value)).setDynamicTooltip();
174
+
175
+ if (commitOnChange) {
176
+ // Reactive: commit on every change
177
+ slider.onChange(async (newValue) => {
178
+ await this.updateSetting(key as keyof z.infer<TSchema>, newValue);
179
+ });
180
+ } else {
181
+ // Commit only when user finishes dragging
182
+ const commit = async (newValue: number) => {
183
+ try {
184
+ await this.updateSetting(key as keyof z.infer<TSchema>, newValue);
185
+ } catch (error) {
186
+ new Notice(`Invalid input: ${error}`, 5000);
187
+ }
188
+ };
189
+
190
+ // Update tooltip during drag for visual feedback
191
+ slider.onChange((newValue) => {
192
+ slider.sliderEl.setAttribute("aria-valuenow", String(newValue));
193
+ });
194
+
195
+ // Commit on mouse up
196
+ slider.sliderEl.addEventListener("mouseup", () => {
197
+ void commit(Number(slider.sliderEl.value));
198
+ });
199
+
200
+ // Commit on keyboard navigation
201
+ slider.sliderEl.addEventListener("keyup", (e: KeyboardEvent) => {
202
+ if (
203
+ ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)
204
+ ) {
205
+ void commit(Number(slider.sliderEl.value));
206
+ }
207
+ });
208
+ }
209
+
210
+ return slider;
211
+ });
212
+ }
213
+
214
+ addText(containerEl: HTMLElement, config: TextSettingConfig): void {
215
+ const { key, name, desc, placeholder = "", commitOnChange = false } = config;
216
+ const value = this.settings[key as keyof z.infer<TSchema>];
217
+
218
+ new Setting(containerEl)
219
+ .setName(name)
220
+ .setDesc(desc)
221
+ .addText((text) => {
222
+ text.setPlaceholder(placeholder);
223
+ text.setValue(String(value ?? ""));
224
+
225
+ if (commitOnChange) {
226
+ // Reactive: commit on every change
227
+ text.onChange(async (newValue) => {
228
+ await this.updateSetting(key as keyof z.infer<TSchema>, newValue);
229
+ });
230
+ } else {
231
+ // Commit only on blur or Ctrl/Cmd+Enter
232
+ const commit = async (inputValue: string) => {
233
+ try {
234
+ await this.updateSetting(key as keyof z.infer<TSchema>, inputValue);
235
+ } catch (error) {
236
+ new Notice(`Invalid input: ${error}`, 5000);
237
+ }
238
+ };
239
+
240
+ text.inputEl.addEventListener("blur", () => void commit(text.inputEl.value));
241
+ text.inputEl.addEventListener("keydown", (e: KeyboardEvent) => {
242
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
243
+ e.preventDefault();
244
+ void commit(text.inputEl.value);
245
+ }
246
+ });
247
+ }
248
+ });
249
+ }
250
+
251
+ addDropdown(containerEl: HTMLElement, config: DropdownSettingConfig): void {
252
+ const { key, name, desc, options } = config;
253
+ const value = this.settings[key as keyof z.infer<TSchema>];
254
+
255
+ new Setting(containerEl)
256
+ .setName(name)
257
+ .setDesc(desc)
258
+ .addDropdown((dropdown) =>
259
+ dropdown
260
+ .addOptions(options)
261
+ .setValue(String(value))
262
+ .onChange(async (newValue) => {
263
+ await this.updateSetting(key as keyof z.infer<TSchema>, newValue);
264
+ })
265
+ );
266
+ }
267
+
268
+ addTextArray<T = string>(containerEl: HTMLElement, config: ArraySettingConfig<T>): void {
269
+ const {
270
+ key,
271
+ name,
272
+ desc,
273
+ placeholder = "",
274
+ arrayDelimiter = ", ",
275
+ multiline = false,
276
+ commitOnChange = false,
277
+ } = config;
278
+ const value = this.settings[key as keyof z.infer<TSchema>] as T[];
279
+
280
+ const inferredItemType = config.itemType ?? this.inferArrayItemType(key) ?? "string";
281
+ const parser =
282
+ config.parser ??
283
+ ((input: string) => {
284
+ if (inferredItemType === "number") {
285
+ const num = Number(input);
286
+ if (Number.isNaN(num)) {
287
+ throw new Error(`Invalid number: ${input}`);
288
+ }
289
+ return num as T;
290
+ }
291
+ return input as T;
292
+ });
293
+
294
+ const validator = config.validator ?? ((_item: T) => true);
295
+
296
+ const setting = new Setting(containerEl).setName(name).setDesc(desc);
297
+
298
+ if (multiline) {
299
+ setting.addTextArea((text) => {
300
+ text.setPlaceholder(placeholder);
301
+ text.setValue(Array.isArray(value) ? value.join("\n") : "");
302
+
303
+ const commit = async (inputValue: string) => {
304
+ const lines = inputValue
305
+ .split("\n")
306
+ .map((s) => s.trim())
307
+ .filter((s) => s.length > 0);
308
+
309
+ try {
310
+ const items = lines.map(parser).filter(validator);
311
+ await this.updateSetting(key as keyof z.infer<TSchema>, items);
312
+ } catch (error) {
313
+ new Notice(`Invalid input: ${error}`, 5000);
314
+ }
315
+ };
316
+
317
+ if (commitOnChange) {
318
+ // Reactive: commit on every change
319
+ text.onChange(async (inputValue) => {
320
+ await commit(inputValue);
321
+ });
322
+ } else {
323
+ // Commit only on blur or Ctrl/Cmd+Enter
324
+ text.inputEl.addEventListener("blur", () => void commit(text.inputEl.value));
325
+ text.inputEl.addEventListener("keydown", (e: KeyboardEvent) => {
326
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
327
+ e.preventDefault();
328
+ void commit(text.inputEl.value);
329
+ }
330
+ });
331
+ }
332
+
333
+ text.inputEl.rows = 5;
334
+ text.inputEl.classList.add("settings-ui-builder-textarea");
335
+ });
336
+ } else {
337
+ setting.addText((text) => {
338
+ text.setPlaceholder(placeholder);
339
+ text.setValue(Array.isArray(value) ? value.join(arrayDelimiter) : "");
340
+
341
+ const commit = async (inputValue: string) => {
342
+ const tokens = inputValue
343
+ .split(",")
344
+ .map((s) => s.trim())
345
+ .filter((s) => s.length > 0);
346
+
347
+ try {
348
+ const items = tokens.map(parser).filter(validator);
349
+ await this.updateSetting(key as keyof z.infer<TSchema>, items);
350
+ } catch (error) {
351
+ new Notice(`Invalid input: ${error}`, 5000);
352
+ }
353
+ };
354
+
355
+ if (commitOnChange) {
356
+ // Reactive: commit on every change
357
+ text.onChange(async (inputValue) => {
358
+ await commit(inputValue);
359
+ });
360
+ } else {
361
+ // Commit only on blur or Ctrl/Cmd+Enter
362
+ text.inputEl.addEventListener("blur", () => void commit(text.inputEl.value));
363
+ text.inputEl.addEventListener("keydown", (e: KeyboardEvent) => {
364
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
365
+ e.preventDefault();
366
+ void commit(text.inputEl.value);
367
+ }
368
+ });
369
+ }
370
+ });
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Advanced array manager with add/remove buttons for each item
376
+ * Similar to the directory settings pattern
377
+ */
378
+ addArrayManager(containerEl: HTMLElement, config: ArrayManagerConfig): void {
379
+ const {
380
+ key,
381
+ name,
382
+ desc,
383
+ placeholder = "",
384
+ addButtonText = "Add",
385
+ removeButtonText = "Remove",
386
+ emptyArrayFallback = [],
387
+ preventEmpty = false,
388
+ itemDescriptionFn,
389
+ onBeforeAdd,
390
+ onBeforeRemove,
391
+ quickActions = [],
392
+ } = config;
393
+
394
+ // Section heading
395
+ new Setting(containerEl).setName(name).setHeading();
396
+
397
+ // Description
398
+ if (desc) {
399
+ const descEl = containerEl.createDiv("setting-item-description");
400
+ descEl.setText(desc);
401
+ }
402
+
403
+ // Container for list items
404
+ const listContainer = containerEl.createDiv("settings-array-manager-list");
405
+
406
+ const render = () => {
407
+ listContainer.empty();
408
+
409
+ const currentItems = (this.settings[key as keyof z.infer<TSchema>] as unknown[]) ?? [];
410
+
411
+ for (const item of currentItems) {
412
+ const itemSetting = new Setting(listContainer).setName(String(item)).addButton((button) =>
413
+ button
414
+ .setButtonText(removeButtonText)
415
+ .setWarning()
416
+ .onClick(async () => {
417
+ let newItems = currentItems.filter((i) => i !== item);
418
+
419
+ // Apply custom logic before removal
420
+ if (onBeforeRemove) {
421
+ newItems = await onBeforeRemove(item, currentItems);
422
+ }
423
+
424
+ // Prevent empty array if configured
425
+ if (preventEmpty && newItems.length === 0) {
426
+ newItems = Array.isArray(emptyArrayFallback)
427
+ ? emptyArrayFallback
428
+ : [emptyArrayFallback];
429
+ }
430
+
431
+ await this.updateSetting(key as keyof z.infer<TSchema>, newItems);
432
+ render();
433
+ })
434
+ );
435
+
436
+ // Add custom description for each item if provided
437
+ if (itemDescriptionFn) {
438
+ itemSetting.setDesc(itemDescriptionFn(item));
439
+ }
440
+ }
441
+ };
442
+
443
+ render();
444
+
445
+ // Add new item section
446
+ const inputId = `settings-array-manager-input-${key}`;
447
+ new Setting(containerEl)
448
+ .setName(`Add ${name.toLowerCase()}`)
449
+ .setDesc(`Enter a new value`)
450
+ .addText((text) => {
451
+ text.setPlaceholder(placeholder);
452
+ text.inputEl.id = inputId;
453
+ })
454
+ .addButton((button) =>
455
+ button
456
+ .setButtonText(addButtonText)
457
+ .setCta()
458
+ .onClick(async () => {
459
+ const input = containerEl.querySelector(`#${inputId}`) as HTMLInputElement;
460
+ const newItem = input.value.trim();
461
+
462
+ if (!newItem) {
463
+ return;
464
+ }
465
+
466
+ const currentItems = (this.settings[key as keyof z.infer<TSchema>] as unknown[]) ?? [];
467
+ let newItems = [...currentItems];
468
+
469
+ // Apply custom logic before adding
470
+ if (onBeforeAdd) {
471
+ newItems = await onBeforeAdd(newItem, currentItems);
472
+ } else {
473
+ // Default behavior: add if not exists
474
+ if (!newItems.includes(newItem)) {
475
+ newItems.push(newItem);
476
+ }
477
+ }
478
+
479
+ await this.updateSetting(key as keyof z.infer<TSchema>, newItems);
480
+ input.value = "";
481
+ render();
482
+ })
483
+ );
484
+
485
+ // Quick actions
486
+ for (const quickAction of quickActions) {
487
+ const currentItems = (this.settings[key as keyof z.infer<TSchema>] as unknown[]) ?? [];
488
+
489
+ // Check condition if provided
490
+ if (quickAction.condition && !quickAction.condition(currentItems)) {
491
+ continue;
492
+ }
493
+
494
+ new Setting(containerEl)
495
+ .setName(quickAction.name)
496
+ .setDesc(quickAction.desc)
497
+ .addButton((button) =>
498
+ button.setButtonText(quickAction.buttonText).onClick(async () => {
499
+ const currentItems = (this.settings[key as keyof z.infer<TSchema>] as unknown[]) ?? [];
500
+ const newItems = await quickAction.action(currentItems);
501
+ await this.updateSetting(key as keyof z.infer<TSchema>, newItems);
502
+ render();
503
+ })
504
+ );
505
+ }
506
+ }
507
+ }
@@ -0,0 +1 @@
1
+ export * from "./string";