diff --git a/bun.lock b/bun.lock index ba9a17f2..5650b018 100644 --- a/bun.lock +++ b/bun.lock @@ -55,7 +55,7 @@ "@types/codemirror": "^5.60.17", "itertools-ts": "^2.5.0", "meta-bind-core": "workspace:*", - "obsidian": "latest", + "obsidian": "obsidianmd/obsidian-api#2d878c6a67294b13a07908b27003246a8593d469", "svelte": "^5.55.5", "zod": "^4.3.6", }, @@ -711,7 +711,7 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "obsidian": ["obsidian@github:obsidianmd/obsidian-api#2d878c6", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "obsidianmd-obsidian-api-2d878c6", "sha512-XKIdu7LIFDBs8yFebEs4ML+XvBAw+8vaQ8qBHd8TaRXMADAtG1UVJo4t/t6R8DdKhuB7XpPuYctQOKdH2lAsKg=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], @@ -965,6 +965,8 @@ "eslint-plugin-obsidianmd/@types/node": ["@types/node@20.12.12", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw=="], + "eslint-plugin-obsidianmd/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "eslint-plugin-obsidianmd/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -977,6 +979,8 @@ "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + "meta-bind-publish/obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "obsidian/@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], @@ -1005,8 +1009,16 @@ "eslint-plugin-obsidianmd/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "eslint-plugin-obsidianmd/obsidian/@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], + + "eslint-plugin-obsidianmd/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "json-schema-migrate/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "meta-bind-publish/obsidian/@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], + + "meta-bind-publish/obsidian/moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "vite-plugin-static-copy/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "vite-plugin-static-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], diff --git a/manifest-beta.json b/manifest-beta.json index 4a38c1b5..2abc6b27 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -2,7 +2,7 @@ "id": "obsidian-meta-bind-plugin", "name": "Meta Bind", "version": "1.4.10", - "minAppVersion": "1.10.0", + "minAppVersion": "1.13.0", "description": "Make your notes interactive with inline input fields, metadata displays, and buttons.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev/", diff --git a/manifest.json b/manifest.json index 4a38c1b5..2abc6b27 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "obsidian-meta-bind-plugin", "name": "Meta Bind", "version": "1.4.10", - "minAppVersion": "1.10.0", + "minAppVersion": "1.13.0", "description": "Make your notes interactive with inline input fields, metadata displays, and buttons.", "author": "Moritz Jung", "authorUrl": "https://www.moritzjung.dev/", diff --git a/packages/core/src/Settings.ts b/packages/core/src/Settings.ts index ca087380..baed523e 100644 --- a/packages/core/src/Settings.ts +++ b/packages/core/src/Settings.ts @@ -59,11 +59,34 @@ export const weekdays: Weekday[] = [ }, ]; +export function getWeekdayByName(name: string): Weekday { + return weekdays.find(weekday => weekday.name === name) ?? weekdays[1]; +} + +export function normalizeFirstWeekday(firstWeekday: unknown): string { + if (typeof firstWeekday === 'string') { + return getWeekdayByName(firstWeekday).name; + } + + if (typeof firstWeekday === 'number') { + return (weekdays.find(weekday => weekday.index === firstWeekday) ?? weekdays[1]).name; + } + + if (typeof firstWeekday === 'object' && firstWeekday !== null && 'name' in firstWeekday) { + const name = firstWeekday.name; + if (typeof name === 'string') { + return getWeekdayByName(name).name; + } + } + + return weekdays[1].name; +} + export interface MetaBindPluginSettings { devMode: boolean; ignoreCodeBlockRestrictions: boolean; preferredDateFormat: string; - firstWeekday: Weekday; + firstWeekday: string; syncInterval: number; enableJs: boolean; viewFieldDisplayNullAsEmpty: boolean; @@ -84,7 +107,7 @@ export const DEFAULT_SETTINGS: MetaBindPluginSettings = { devMode: false, ignoreCodeBlockRestrictions: false, preferredDateFormat: 'YYYY-MM-DD', - firstWeekday: weekdays[1], + firstWeekday: weekdays[1].name, syncInterval: 200, enableJs: false, viewFieldDisplayNullAsEmpty: false, diff --git a/packages/core/src/utils/DatePickerUtils.ts b/packages/core/src/utils/DatePickerUtils.ts index eff63c28..a5f3434d 100644 --- a/packages/core/src/utils/DatePickerUtils.ts +++ b/packages/core/src/utils/DatePickerUtils.ts @@ -1,12 +1,12 @@ import type { Weekday } from 'meta-bind-core/src/Settings'; -import { monthNames, weekdays } from 'meta-bind-core/src/Settings'; +import { getWeekdayByName, monthNames, weekdays } from 'meta-bind-core/src/Settings'; import { mod } from 'meta-bind-core/src/utils/Utils'; import Moment from 'moment/moment'; export let firstWeekday: Weekday = weekdays[1]; -export function setFirstWeekday(w: Weekday): void { - firstWeekday = w; +export function setFirstWeekday(weekdayName: string): void { + firstWeekday = getWeekdayByName(weekdayName); } export function getMonthName(index: number): string { diff --git a/packages/core/tests/utils/Settings.test.ts b/packages/core/tests/utils/Settings.test.ts new file mode 100644 index 00000000..98a48462 --- /dev/null +++ b/packages/core/tests/utils/Settings.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from 'bun:test'; +import { normalizeFirstWeekday } from 'meta-bind-core/src/Settings'; + +describe('settings', () => { + describe('normalizeFirstWeekday', () => { + test('keeps valid weekday names', () => { + expect(normalizeFirstWeekday('Sunday')).toBe('Sunday'); + expect(normalizeFirstWeekday('Wednesday')).toBe('Wednesday'); + }); + + test('migrates legacy weekday objects', () => { + expect(normalizeFirstWeekday({ index: 5, name: 'Friday', shortName: 'Fr' })).toBe('Friday'); + }); + + test('accepts numeric weekday indexes defensively', () => { + expect(normalizeFirstWeekday(6)).toBe('Saturday'); + }); + + test('falls back to Monday for invalid values', () => { + expect(normalizeFirstWeekday('Funday')).toBe('Monday'); + expect(normalizeFirstWeekday({ name: 1 })).toBe('Monday'); + expect(normalizeFirstWeekday(undefined)).toBe('Monday'); + }); + }); +}); diff --git a/packages/obsidian/package.json b/packages/obsidian/package.json index 1ebff96e..7b009c4e 100644 --- a/packages/obsidian/package.json +++ b/packages/obsidian/package.json @@ -21,6 +21,6 @@ "@codemirror/lint": "^6.9.5", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.41.1", - "obsidian": "latest" + "obsidian": "obsidianmd/obsidian-api#2d878c6a67294b13a07908b27003246a8593d469" } } diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts index d2b3e38a..60a0a42e 100644 --- a/packages/obsidian/src/main.ts +++ b/packages/obsidian/src/main.ts @@ -1,5 +1,5 @@ import type { MetaBindPluginSettings } from 'meta-bind-core/src/Settings'; -import { DEFAULT_SETTINGS } from 'meta-bind-core/src/Settings'; +import { DEFAULT_SETTINGS, normalizeFirstWeekday } from 'meta-bind-core/src/Settings'; import { areObjectsEqual } from 'meta-bind-core/src/utils/Utils'; import type { ObsAPI } from 'meta-bind-obsidian/src/ObsAPI'; import { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; @@ -40,13 +40,15 @@ export default class ObsMetaBindPlugin extends Plugin { async loadSettings(): Promise { MB_DEBUG && console.log(`meta-bind | Main >> loading settings`); - const loadedSettings = ((await this.loadData()) ?? {}) as MetaBindPluginSettings; + const loadedSettings = ((await this.loadData()) ?? {}) as Partial; if (typeof loadedSettings === 'object' && loadedSettings != null) { // @ts-expect-error TS2339 remove old config field delete loadedSettings.inputTemplates; // @ts-expect-error TS2339 remove old config field delete loadedSettings.useUsDateInputOrder; + + loadedSettings.firstWeekday = normalizeFirstWeekday(loadedSettings.firstWeekday); } this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings); diff --git a/packages/obsidian/src/settings/ListSettingGroup.ts b/packages/obsidian/src/settings/ListSettingGroup.ts new file mode 100644 index 00000000..3e48a30b --- /dev/null +++ b/packages/obsidian/src/settings/ListSettingGroup.ts @@ -0,0 +1,61 @@ +import type { MetaBindSettingKey } from 'meta-bind-obsidian/src/settings/SettingsTypes'; +import type { SettingDefinitionItem, SettingGroupItem } from 'obsidian'; + +export interface ListSettingGroupOptions { + heading: string; + emptyState: string; + items: T[]; + renderItem: (item: T, index: number) => SettingGroupItem; + applyItems: (items: T[]) => boolean; + onUpdate: () => void; +} + +export function getListSettingGroup(options: ListSettingGroupOptions): SettingDefinitionItem { + return { + type: 'group', + heading: options.heading, + cls: 'mod-list', + emptyState: options.emptyState, + items: options.items.map((item, index) => options.renderItem(item, index)), + onDelete: (index: number): void => { + updateListItems( + options.items, + items => { + items.splice(index, 1); + }, + options.applyItems, + options.onUpdate, + ); + }, + onReorder: (oldIndex: number, newIndex: number): void => { + updateListItems( + options.items, + items => { + moveListItem(items, oldIndex, newIndex); + }, + options.applyItems, + options.onUpdate, + ); + }, + }; +} + +export function updateListItems( + items: T[], + updateItems: (items: T[]) => void, + applyItems: (items: T[]) => boolean, + onUpdate: () => void, +): boolean { + const nextItems = structuredClone(items); + updateItems(nextItems); + if (applyItems(nextItems)) { + onUpdate(); + return true; + } + return false; +} + +function moveListItem(items: T[], oldIndex: number, newIndex: number): void { + const [item] = items.splice(oldIndex, 1); + items.splice(newIndex, 0, item); +} diff --git a/packages/obsidian/src/settings/SettingsTab.ts b/packages/obsidian/src/settings/SettingsTab.ts index d52a6877..3ca9a798 100644 --- a/packages/obsidian/src/settings/SettingsTab.ts +++ b/packages/obsidian/src/settings/SettingsTab.ts @@ -1,42 +1,160 @@ import { MetaBindBuild } from 'meta-bind-core/src'; +import type { MetaBindPluginSettings } from 'meta-bind-core/src/Settings'; import { DEFAULT_SETTINGS, MAX_SYNC_INTERVAL, MIN_SYNC_INTERVAL, weekdays } from 'meta-bind-core/src/Settings'; import { DocsUtils } from 'meta-bind-core/src/utils/DocsUtils'; import type { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; import { MB_PLAYGROUND_VIEW_TYPE } from 'meta-bind-obsidian/src/playground/PlaygroundView'; -import { ButtonTemplatesSettingModal } from 'meta-bind-obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingModal'; -import { ExcludedFoldersSettingModal } from 'meta-bind-obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingModal'; -import { InputFieldTemplatesSettingModal } from 'meta-bind-obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingModal'; -import type { App } from 'obsidian'; -import { ButtonComponent, PluginSettingTab, Setting } from 'obsidian'; - -export class MetaBindSettingTab extends PluginSettingTab { +import { ButtonTemplateSettings } from 'meta-bind-obsidian/src/settings/buttonTemplateSetting/ButtonTemplateSettings'; +import { ExcludedFolderSettings } from 'meta-bind-obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettings'; +import { InputFieldTemplateSettings } from 'meta-bind-obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettings'; +import type { MetaBindSettingKey } from 'meta-bind-obsidian/src/settings/SettingsTypes'; +import type { App, Setting, SettingControlBinding, SettingDefinitionItem } from 'obsidian'; +import { PluginSettingTab } from 'obsidian'; + +export class MetaBindSettingTab extends PluginSettingTab { mb: ObsMetaBind; constructor(app: App, mb: ObsMetaBind) { - super(app, mb.plugin); + super(app, mb.plugin, mb.plugin.settings); this.mb = mb; } - display(): void { - const { containerEl } = this; - - containerEl.empty(); + getSettingDefinitions(): SettingDefinitionItem[] { + const items: SettingDefinitionItem[] = []; if (this.mb.build === MetaBindBuild.DEV || this.mb.build === MetaBindBuild.CANARY) { - containerEl.createEl('p', { - text: `You are using a ${this.mb.build} build (${MB_VERSION}). This build is not intended for production use. Use at your own risk.`, - cls: 'mb-error', - }); - const button = new ButtonComponent(containerEl); - button.setButtonText('Learn about canary builds'); - button.setCta(); - button.onClick(() => { - DocsUtils.open(DocsUtils.linkToCanaryBuilds()); + items.push({ + name: 'Development build', + desc: `You are using a ${this.mb.build} build (${MB_VERSION}). This build is not intended for production use. Use at your own risk.`, + render: (setting: Setting): void => { + setting.setClass('mb-error'); + setting.addButton(cb => { + cb.setCta(); + cb.setButtonText('Learn about canary builds'); + cb.onClick(() => { + DocsUtils.open(DocsUtils.linkToCanaryBuilds()); + }); + }); + }, }); } - new Setting(containerEl) - .setName('Quick access') + items.push( + { + name: 'Quick access', + render: (setting: Setting): void => { + this.addQuickAccessButtons(setting); + }, + }, + { + name: 'Enable syntax highlighting', + desc: 'Enable syntax highlighting for meta bind syntax. Restart required.', + control: { type: 'toggle', key: 'enableSyntaxHighlighting' }, + }, + { + name: 'Enable editor right-click menu', + desc: 'Enable a meta bind menu section in the editor right-click menu. Restart required.', + control: { type: 'toggle', key: 'enableEditorRightClickMenu' }, + }, + { + type: 'page', + name: 'Input field templates', + desc: 'You can specify input field templates here, and access them using `INPUT[template_name][overrides (optional)]` in your notes.', + items: new InputFieldTemplateSettings(this.app, this.mb, () => this.update()).getDefinitions(), + }, + { + type: 'page', + name: 'Button templates', + desc: 'You can specify button field templates here, and access them in inline buttons.', + items: new ButtonTemplateSettings(this.mb, () => this.update()).getDefinitions(), + }, + { + type: 'page', + name: 'Excluded folders', + desc: 'You can specify excluded folders here. The plugin will not work within excluded folders.', + items: new ExcludedFolderSettings(this.app, this.mb, () => this.update()).getDefinitions(), + }, + { + name: 'View fields display null as empty', + desc: 'Display nothing instead of null, if the frontmatter value is empty, in text view fields.', + control: { type: 'toggle', key: 'viewFieldDisplayNullAsEmpty' }, + }, + { + name: 'Enable JavaScript', + desc: "Enable features that run user written JavaScript. This is potentially DANGEROUS, thus it's disabled by default. Restart required.", + control: { type: 'toggle', key: 'enableJs' }, + }, + { + type: 'group', + heading: 'Date and time', + items: [ + { + name: 'Date format', + desc: 'The date format to be used by this plugin. Changing this setting will break the parsing of existing date inputs. Here is a list of all available date tokes https://momentjs.com/docs/#/displaying/.', + control: { type: 'text', key: 'preferredDateFormat' }, + }, + { + name: 'First weekday', + desc: 'Specify the first weekday for the datepicker.', + control: { + type: 'dropdown', + key: 'firstWeekday', + options: Object.fromEntries(weekdays.map(weekday => [weekday.name, weekday.name])), + }, + }, + ], + }, + { + type: 'group', + heading: 'Advanced', + items: [ + { + name: 'Dev mode', + desc: 'Enable dev mode. Not recommended unless you want to debug this plugin.', + control: { type: 'toggle', key: 'devMode' }, + }, + { + name: 'Disable code block restrictions', + desc: 'Disable restrictions on which input fields can be created in which code blocks. Not recommended unless you know what you are doing.', + control: { type: 'toggle', key: 'ignoreCodeBlockRestrictions' }, + }, + { + name: 'Sync interval', + desc: `The interval in milli-seconds between disk writes. Changing this number is not recommended except if your hard drive is exceptionally slow. Standard: ${DEFAULT_SETTINGS.syncInterval}; Minimum: ${MIN_SYNC_INTERVAL}; Maximum: ${MAX_SYNC_INTERVAL}`, + control: { + type: 'number', + key: 'syncInterval', + defaultValue: DEFAULT_SETTINGS.syncInterval, + min: MIN_SYNC_INTERVAL, + max: MAX_SYNC_INTERVAL, + step: 1, + validate: (value: number): string | void => { + if (value < MIN_SYNC_INTERVAL || value > MAX_SYNC_INTERVAL) { + return `Sync interval must be between ${MIN_SYNC_INTERVAL} and ${MAX_SYNC_INTERVAL}.`; + } + }, + }, + }, + ], + }, + ); + + return items; + } + + getControlBinding(key: MetaBindSettingKey): SettingControlBinding { + return { + value: this.mb.getSettings()[key], + onChange: (value: unknown): void => { + this.mb.updateSettings(settings => { + settings[key] = value as never; + }); + }, + }; + } + + private addQuickAccessButtons(setting: Setting): void { + setting .addButton(cb => { cb.setCta(); cb.setButtonText('Docs'); @@ -62,169 +180,5 @@ export class MetaBindSettingTab extends PluginSettingTab { DocsUtils.open(DocsUtils.linkToIssues()); }); }); - - new Setting(containerEl) - .setName('Enable syntax highlighting') - .setDesc(`Enable syntax highlighting for meta bind syntax. Restart required.`) - .addToggle(cb => { - cb.setValue(this.mb.getSettings().enableSyntaxHighlighting); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.enableSyntaxHighlighting = data; - }); - }); - }); - - new Setting(containerEl) - .setName('Enable editor right-click menu') - .setDesc(`Enable a meta bind menu section in the editor right-click menu. Restart required.`) - .addToggle(cb => { - cb.setValue(this.mb.getSettings().enableEditorRightClickMenu); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.enableEditorRightClickMenu = data; - }); - }); - }); - - new Setting(containerEl) - .setName('Input field templates') - .setDesc( - `You can specify input field templates here, and access them using \`INPUT[template_name][overrides (optional)]\` in your notes.`, - ) - .addButton(cb => { - cb.setButtonText('Edit templates'); - cb.onClick(() => { - new InputFieldTemplatesSettingModal(this.app, this.mb).open(); - }); - }); - - new Setting(containerEl) - .setName('Button templates') - .setDesc(`You can specify button field templates here, and access them in inline buttons.`) - .addButton(cb => { - cb.setButtonText('Edit templates'); - cb.onClick(() => { - new ButtonTemplatesSettingModal(this.app, this.mb).open(); - }); - }); - - new Setting(containerEl) - .setName('Excluded folders') - .setDesc(`You can specify excluded folders here. The plugin will not work within excluded folders.`) - .addButton(cb => { - cb.setButtonText('Edit excluded folders'); - cb.onClick(() => { - new ExcludedFoldersSettingModal(this.app, this.mb).open(); - }); - }); - - new Setting(containerEl) - .setName('View fields display null as empty') - .setDesc('Display nothing instead of null, if the frontmatter value is empty, in text view fields.') - .addToggle(cb => { - cb.setValue(this.mb.getSettings().viewFieldDisplayNullAsEmpty); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.viewFieldDisplayNullAsEmpty = data; - }); - }); - }); - - new Setting(containerEl) - .setName('Enable JavaScript') - .setDesc( - "Enable features that run user written JavaScript. This is potentially DANGEROUS, thus it's disabled by default. Restart required.", - ) - .addToggle(cb => { - cb.setValue(this.mb.getSettings().enableJs); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.enableJs = data; - }); - }); - }); - - new Setting(containerEl).setName('Date and time').setHeading(); - - new Setting(containerEl) - .setName('Date format') - .setDesc( - `The date format to be used by this plugin. Changing this setting will break the parsing of existing date inputs. Here is a list of all available date tokes https://momentjs.com/docs/#/displaying/.`, - ) - .addText(cb => { - cb.setValue(this.mb.getSettings().preferredDateFormat); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.preferredDateFormat = data; - }); - }); - }); - - new Setting(containerEl) - .setName('First weekday') - .setDesc(`Specify the first weekday for the datepicker.`) - .addDropdown(cb => { - for (const weekday of weekdays) { - cb.addOption(weekday.name, weekday.name); - } - cb.setValue(this.mb.getSettings().firstWeekday.name); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.firstWeekday = weekdays.find(x => x.name === data)!; - }); - }); - }); - - new Setting(containerEl).setName('Advanced').setHeading(); - - new Setting(containerEl) - .setName('Dev mode') - .setDesc('Enable dev mode. Not recommended unless you want to debug this plugin.') - .addToggle(cb => { - cb.setValue(this.mb.getSettings().devMode); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.devMode = data; - }); - }); - }); - - new Setting(containerEl) - .setName('Disable code block restrictions') - .setDesc( - 'Disable restrictions on which input fields can be created in which code blocks. Not recommended unless you know what you are doing.', - ) - .addToggle(cb => { - cb.setValue(this.mb.getSettings().ignoreCodeBlockRestrictions); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.ignoreCodeBlockRestrictions = data; - }); - }); - }); - - new Setting(containerEl) - .setName('Sync interval') - .setDesc( - `The interval in milli-seconds between disk writes. Changing this number is not recommended except if your hard drive is exceptionally slow. Standard: ${DEFAULT_SETTINGS.syncInterval}; Minimum: ${MIN_SYNC_INTERVAL}; Maximum: ${MAX_SYNC_INTERVAL}`, - ) - .addText(cb => { - cb.setValue(this.mb.getSettings().syncInterval.toString()); - cb.onChange(data => { - this.mb.updateSettings(settings => { - settings.syncInterval = Number.parseInt(data); - if (Number.isNaN(settings.syncInterval)) { - settings.syncInterval = DEFAULT_SETTINGS.syncInterval; - } - if (settings.syncInterval < MIN_SYNC_INTERVAL) { - settings.syncInterval = MIN_SYNC_INTERVAL; - } - if (settings.syncInterval > MAX_SYNC_INTERVAL) { - settings.syncInterval = MAX_SYNC_INTERVAL; - } - }); - }); - }); } } diff --git a/packages/obsidian/src/settings/SettingsTypes.ts b/packages/obsidian/src/settings/SettingsTypes.ts new file mode 100644 index 00000000..031a8ccb --- /dev/null +++ b/packages/obsidian/src/settings/SettingsTypes.ts @@ -0,0 +1,3 @@ +import type { MetaBindPluginSettings } from 'meta-bind-core/src/Settings'; + +export type MetaBindSettingKey = keyof MetaBindPluginSettings; diff --git a/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplateSettingComponent.svelte b/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplateSettingComponent.svelte deleted file mode 100644 index 9e92d0e5..00000000 --- a/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplateSettingComponent.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - -
- - {template.id} - - - - -
{stringifyYaml(template)}
-
diff --git a/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplateSettings.ts b/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplateSettings.ts new file mode 100644 index 00000000..20344bd8 --- /dev/null +++ b/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplateSettings.ts @@ -0,0 +1,169 @@ +import type { ButtonConfig } from 'meta-bind-core/src/config/ButtonConfig'; +import type { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; +import { getListSettingGroup, updateListItems } from 'meta-bind-obsidian/src/settings/ListSettingGroup'; +import type { MetaBindSettingKey } from 'meta-bind-obsidian/src/settings/SettingsTypes'; +import type { Setting, SettingDefinitionItem, SettingGroupItem } from 'obsidian'; +import { Notice, parseYaml, stringifyYaml } from 'obsidian'; + +export class ButtonTemplateSettings { + constructor( + private readonly mb: ObsMetaBind, + private readonly onUpdate: () => void, + ) {} + + getDefinitions(): SettingDefinitionItem[] { + const templates = this.mb.getSettings().buttonTemplates; + + return [ + { + name: 'Add template', + action: (): void => { + this.mb.internal.openButtonBuilderModal({ + submitText: 'Add', + config: this.createDefaultButtonTemplate(), + onOkay: newTemplate => { + updateListItems( + this.mb.getSettings().buttonTemplates, + templates => { + templates.push(newTemplate); + }, + templates => this.applyButtonTemplates(templates), + this.onUpdate, + ); + }, + }); + }, + }, + { + name: 'Add template from clipboard', + action: (): void => { + void this.addButtonTemplateFromClipboard(); + }, + }, + getListSettingGroup({ + heading: 'Templates', + emptyState: 'No button templates configured.', + items: templates, + renderItem: (template, index) => this.getButtonTemplateSetting(template, index), + applyItems: templates => this.applyButtonTemplates(templates), + onUpdate: this.onUpdate, + }), + ]; + } + + private getButtonTemplateSetting(template: ButtonConfig, index: number): SettingGroupItem { + const actionCount = template.actions?.length ?? (template.action ? 1 : 0); + + return { + name: template.id ?? template.label ?? `Button template ${index + 1}`, + desc: `${template.label ?? 'No label'} - ${actionCount} action${actionCount === 1 ? '' : 's'}`, + searchable: true, + aliases: [template.id, template.label, template.tooltip].filter(x => x !== undefined), + render: (setting: Setting): void => { + setting.addExtraButton(cb => { + cb.setIcon('pencil'); + cb.setTooltip('Edit template'); + cb.onClick(() => { + this.mb.internal.openButtonBuilderModal({ + submitText: 'Submit', + config: structuredClone(template), + onOkay: newTemplate => { + updateListItems( + this.mb.getSettings().buttonTemplates, + templates => { + templates[index] = newTemplate; + }, + templates => this.applyButtonTemplates(templates), + this.onUpdate, + ); + }, + }); + }); + }); + setting.addExtraButton(cb => { + cb.setIcon('copy'); + cb.setTooltip('Copy template YAML'); + cb.onClick(() => { + void navigator.clipboard.writeText(stringifyYaml(template)); + new Notice('meta-bind | Copied to clipboard'); + }); + }); + }, + }; + } + + private applyButtonTemplates(templates: ButtonConfig[]): boolean { + const previousTemplates = this.mb.getSettings().buttonTemplates; + const errorCollection = this.mb.buttonManager.setButtonTemplates(templates); + if (errorCollection.hasErrors()) { + const errors = errorCollection.getErrors(); + this.mb.buttonManager.setButtonTemplates(previousTemplates); + console.warn('meta-bind | failed to save button templates', errors); + new Notice( + `meta-bind | Button template could not be saved: ${errors[0]?.message ?? 'Unknown validation error.'}`, + ); + return false; + } + + this.mb.updateSettings(settings => { + settings.buttonTemplates = templates; + }); + + return true; + } + + private async addButtonTemplateFromClipboard(): Promise { + let template: ButtonConfig; + try { + template = parseYaml(await navigator.clipboard.readText()) as ButtonConfig; + if (template.id === undefined || template.id === '') { + template.id = this.createUniqueButtonTemplateId(); + } + } catch (e) { + console.warn(e); + new Notice('meta-bind | Can not parse button config. Check your button syntax.'); + return; + } + + this.mb.internal.openButtonBuilderModal({ + submitText: 'Add', + config: template, + onOkay: newTemplate => { + updateListItems( + this.mb.getSettings().buttonTemplates, + templates => { + templates.push(newTemplate); + }, + templates => this.applyButtonTemplates(templates), + this.onUpdate, + ); + }, + }); + } + + private createDefaultButtonTemplate(): ButtonConfig { + return { + ...this.mb.buttonActionRunner.createDefaultButtonConfig(), + id: this.createUniqueButtonTemplateId(), + }; + } + + private createUniqueButtonTemplateId(): string { + const ids = new Set( + this.mb + .getSettings() + .buttonTemplates.map(template => template.id) + .filter(x => x), + ); + const baseId = 'button-template'; + let id = baseId; + let counter = 2; + + while (ids.has(id)) { + id = `${baseId}-${counter}`; + counter += 1; + } + + return id; + } +} diff --git a/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingComponent.svelte b/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingComponent.svelte deleted file mode 100644 index 97e15850..00000000 --- a/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingComponent.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - -
-

Meta Bind Button Templates

- - {#each buttonConfigs as _, i} - - {/each} - - - - - {#if errorCollection} -
-

Some Templates Failed to Parse

- - -
- {/if} - - - - - -
diff --git a/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingModal.ts b/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingModal.ts deleted file mode 100644 index 91687f34..00000000 --- a/packages/obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingModal.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { ButtonConfig } from 'meta-bind-core/src/config/ButtonConfig'; -import type { ErrorCollection } from 'meta-bind-core/src/utils/errors/ErrorCollection'; -import type { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; -import type { App } from 'obsidian'; -import { Modal } from 'obsidian'; -import type { Component as SvelteComponent } from 'svelte'; -import { mount, unmount } from 'svelte'; -import ButtonTemplatesSettingComponent from 'meta-bind-obsidian/src/settings/buttonTemplateSetting/ButtonTemplatesSettingComponent.svelte'; - -export class ButtonTemplatesSettingModal extends Modal { - readonly mb: ObsMetaBind; - private component: ReturnType | undefined; - - constructor(app: App, mb: ObsMetaBind) { - super(app); - this.mb = mb; - } - - public onOpen(): void { - this.contentEl.empty(); - if (this.component) { - void unmount(this.component); - } - - this.component = mount(ButtonTemplatesSettingComponent, { - target: this.contentEl, - props: { - buttonConfigs: structuredClone(this.mb.getSettings().buttonTemplates), - modal: this, - }, - }); - } - - public onClose(): void { - this.contentEl.empty(); - if (this.component) { - void unmount(this.component); - } - } - - public save(templates: ButtonConfig[]): ErrorCollection | undefined { - const errorCollection = this.mb.buttonManager.setButtonTemplates(templates); - if (errorCollection.hasErrors()) { - return errorCollection; - } - - this.mb.updateSettings(settings => { - settings.buttonTemplates = templates; - }); - - return undefined; - } -} diff --git a/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettingModal.ts b/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettingModal.ts new file mode 100644 index 00000000..b49fb7ff --- /dev/null +++ b/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettingModal.ts @@ -0,0 +1,53 @@ +import type { App } from 'obsidian'; +import { Modal, Notice, Setting } from 'obsidian'; + +export class ExcludedFolderSettingModal extends Modal { + private readonly initialFolder: string; + private readonly onSubmit: (folder: string) => void; + private draft: string; + + constructor(app: App, folder: string | undefined, onSubmit: (folder: string) => void) { + super(app); + this.initialFolder = folder ?? ''; + this.draft = this.initialFolder; + this.onSubmit = onSubmit; + } + + onOpen(): void { + this.contentEl.empty(); + this.setTitle(this.initialFolder === '' ? 'Add excluded folder' : 'Edit excluded folder'); + + new Setting(this.contentEl).setName('Folder path').addText(cb => { + cb.setPlaceholder('path/to/folder'); + cb.setValue(this.draft); + cb.onChange(value => { + this.draft = value; + }); + }); + + this.addModalButtons(); + } + + private addModalButtons(): void { + new Setting(this.contentEl) + .addButton(cb => { + cb.setCta(); + cb.setButtonText('Save'); + cb.onClick(() => { + if (this.draft === '') { + new Notice('meta-bind | Folder path may not be empty.'); + return; + } + + this.onSubmit(this.draft); + this.close(); + }); + }) + .addButton(cb => { + cb.setButtonText('Cancel'); + cb.onClick(() => { + this.close(); + }); + }); + } +} diff --git a/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettings.ts b/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettings.ts new file mode 100644 index 00000000..8360753f --- /dev/null +++ b/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettings.ts @@ -0,0 +1,81 @@ +import type { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; +import { ExcludedFolderSettingModal } from 'meta-bind-obsidian/src/settings/excludedFoldersSetting/ExcludedFolderSettingModal'; +import { getListSettingGroup, updateListItems } from 'meta-bind-obsidian/src/settings/ListSettingGroup'; +import type { MetaBindSettingKey } from 'meta-bind-obsidian/src/settings/SettingsTypes'; +import type { App, Setting, SettingDefinitionItem, SettingGroupItem } from 'obsidian'; + +export class ExcludedFolderSettings { + constructor( + private readonly app: App, + private readonly mb: ObsMetaBind, + private readonly onUpdate: () => void, + ) {} + + getDefinitions(): SettingDefinitionItem[] { + const folders = this.mb.getSettings().excludedFolders; + + return [ + { + name: 'Add folder', + action: (): void => { + this.openExcludedFolderModal(undefined, folder => { + updateListItems( + this.mb.getSettings().excludedFolders, + folders => { + folders.push(folder); + }, + folders => this.applyExcludedFolders(folders), + this.onUpdate, + ); + }); + }, + }, + getListSettingGroup({ + heading: 'Folders', + emptyState: 'No excluded folders configured.', + items: folders, + renderItem: (folder, index) => this.getExcludedFolderSetting(folder, index), + applyItems: folders => this.applyExcludedFolders(folders), + onUpdate: this.onUpdate, + }), + ]; + } + + private getExcludedFolderSetting(folder: string, index: number): SettingGroupItem { + return { + name: folder || `Excluded folder ${index + 1}`, + desc: folder === '' ? 'Folder path may not be empty.' : 'Plugin behavior is disabled in this folder.', + searchable: folder !== '', + render: (setting: Setting): void => { + setting.addExtraButton(cb => { + cb.setIcon('pencil'); + cb.setTooltip('Edit folder'); + cb.onClick(() => { + this.openExcludedFolderModal(folder, newFolder => { + updateListItems( + this.mb.getSettings().excludedFolders, + folders => { + folders[index] = newFolder; + }, + folders => this.applyExcludedFolders(folders), + this.onUpdate, + ); + }); + }); + }); + }, + }; + } + + private applyExcludedFolders(folders: string[]): boolean { + this.mb.updateSettings(settings => { + settings.excludedFolders = folders; + }); + + return true; + } + + private openExcludedFolderModal(folder: string | undefined, onSubmit: (folder: string) => void): void { + new ExcludedFolderSettingModal(this.app, folder, onSubmit).open(); + } +} diff --git a/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingComponent.svelte b/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingComponent.svelte deleted file mode 100644 index 6ed0fa21..00000000 --- a/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingComponent.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - -
- - - - - - - - - {#each excludedFolders as folder, i (i)} - - - - - {/each} - -
Folder Path
- - - -
- - - - {#if errorCollection} -
-

Some folder paths are invalid

- - -
- {/if} - - - - - -
diff --git a/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingModal.ts b/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingModal.ts deleted file mode 100644 index f0f39121..00000000 --- a/packages/obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingModal.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { ErrorCollection } from 'meta-bind-core/src/utils/errors/ErrorCollection'; -import type { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; -import type { App } from 'obsidian'; -import { Modal } from 'obsidian'; -import type { Component as SvelteComponent } from 'svelte'; -import { mount, unmount } from 'svelte'; -import ExcludedFoldersSettingComponent from 'meta-bind-obsidian/src/settings/excludedFoldersSetting/ExcludedFoldersSettingComponent.svelte'; - -export class ExcludedFoldersSettingModal extends Modal { - private readonly mb: ObsMetaBind; - private component: ReturnType | undefined; - - constructor(app: App, mb: ObsMetaBind) { - super(app); - this.mb = mb; - } - - public onOpen(): void { - this.contentEl.empty(); - if (this.component) { - void unmount(this.component); - } - - this.component = mount(ExcludedFoldersSettingComponent, { - target: this.contentEl, - props: { - excludedFolders: structuredClone(this.mb.getSettings().excludedFolders), - modal: this, - mb: this.mb, - }, - }); - } - - public onClose(): void { - this.contentEl.empty(); - if (this.component) { - void unmount(this.component); - } - } - - public save(folders: string[]): ErrorCollection | undefined { - for (const folder of folders) { - if (folder === '') { - const errorCollection = new ErrorCollection('Excluded folders'); - - errorCollection.add(new Error(`Invalid Folder Path '${folder}'. Folder path may not be empty.`)); - - return errorCollection; - } - } - - this.mb.updateSettings(settings => { - settings.excludedFolders = folders; - }); - - return undefined; - } -} diff --git a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettingComponent.svelte b/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettingComponent.svelte deleted file mode 100644 index 39453042..00000000 --- a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettingComponent.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -
- - - - - -
diff --git a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettingModal.ts b/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettingModal.ts new file mode 100644 index 00000000..65640cd0 --- /dev/null +++ b/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettingModal.ts @@ -0,0 +1,67 @@ +import type { InputFieldTemplate } from 'meta-bind-core/src/Settings'; +import type { App } from 'obsidian'; +import { Modal, Setting } from 'obsidian'; + +export class InputFieldTemplateSettingModal extends Modal { + private readonly initialTemplate: InputFieldTemplate; + private readonly onSubmit: (template: InputFieldTemplate) => boolean | void; + private draft: InputFieldTemplate; + + constructor( + app: App, + template: InputFieldTemplate | undefined, + onSubmit: (template: InputFieldTemplate) => boolean | void, + ) { + super(app); + this.initialTemplate = structuredClone(template ?? { name: '', declaration: '' }); + this.draft = structuredClone(this.initialTemplate); + this.onSubmit = onSubmit; + } + + onOpen(): void { + this.contentEl.empty(); + this.setTitle(this.initialTemplate.name === '' ? 'Add input field template' : 'Edit input field template'); + + new Setting(this.contentEl).setName('Name').addText(cb => { + cb.setPlaceholder('template-name'); + cb.setValue(this.draft.name); + cb.onChange(value => { + this.draft.name = value; + }); + }); + + new Setting(this.contentEl) + .setName('Declaration') + .setDesc('Template declaration used after the template name.') + .addTextArea(cb => { + cb.setPlaceholder('INPUT[slider(addLabels)]'); + cb.setValue(this.draft.declaration); + cb.inputEl.addClass('mb-settings-list-modal-textarea'); + cb.onChange(value => { + this.draft.declaration = value; + }); + }); + + this.addModalButtons(); + } + + private addModalButtons(): void { + new Setting(this.contentEl) + .addButton(cb => { + cb.setCta(); + cb.setButtonText('Save'); + cb.onClick(() => { + if (this.onSubmit(structuredClone(this.draft)) === false) { + return; + } + this.close(); + }); + }) + .addButton(cb => { + cb.setButtonText('Cancel'); + cb.onClick(() => { + this.close(); + }); + }); + } +} diff --git a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettings.ts b/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettings.ts new file mode 100644 index 00000000..430be857 --- /dev/null +++ b/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettings.ts @@ -0,0 +1,102 @@ +import type { InputFieldTemplate } from 'meta-bind-core/src/Settings'; +import type { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; +import { InputFieldTemplateSettingModal } from 'meta-bind-obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplateSettingModal'; +import { getListSettingGroup, updateListItems } from 'meta-bind-obsidian/src/settings/ListSettingGroup'; +import type { MetaBindSettingKey } from 'meta-bind-obsidian/src/settings/SettingsTypes'; +import type { App, Setting, SettingDefinitionItem, SettingGroupItem } from 'obsidian'; +import { Notice } from 'obsidian'; + +export class InputFieldTemplateSettings { + constructor( + private readonly app: App, + private readonly mb: ObsMetaBind, + private readonly onUpdate: () => void, + ) {} + + getDefinitions(): SettingDefinitionItem[] { + const templates = this.mb.getSettings().inputFieldTemplates; + + return [ + { + name: 'Add template', + action: (): void => { + this.openInputFieldTemplateModal(undefined, template => { + return updateListItems( + this.mb.getSettings().inputFieldTemplates, + templates => { + templates.push(template); + }, + templates => this.applyInputFieldTemplates(templates), + this.onUpdate, + ); + }); + }, + }, + getListSettingGroup({ + heading: 'Templates', + emptyState: 'No input field templates configured.', + items: templates, + renderItem: (template, index) => this.getInputFieldTemplateSetting(template, index), + applyItems: templates => this.applyInputFieldTemplates(templates), + onUpdate: this.onUpdate, + }), + ]; + } + + private getInputFieldTemplateSetting( + template: InputFieldTemplate, + index: number, + ): SettingGroupItem { + return { + name: template.name || `Input field template ${index + 1}`, + desc: template.declaration || 'No declaration.', + searchable: true, + aliases: [template.name, template.declaration].filter(x => x !== ''), + render: (setting: Setting): void => { + setting.addExtraButton(cb => { + cb.setIcon('pencil'); + cb.setTooltip('Edit template'); + cb.onClick(() => { + this.openInputFieldTemplateModal(template, newTemplate => { + return updateListItems( + this.mb.getSettings().inputFieldTemplates, + templates => { + templates[index] = newTemplate; + }, + templates => this.applyInputFieldTemplates(templates), + this.onUpdate, + ); + }); + }); + }); + }, + }; + } + + private applyInputFieldTemplates(templates: InputFieldTemplate[]): boolean { + const previousTemplates = this.mb.getSettings().inputFieldTemplates; + const errorCollection = this.mb.inputFieldParser.parseTemplates(templates); + if (errorCollection.hasErrors()) { + const errors = errorCollection.getErrors(); + this.mb.inputFieldParser.parseTemplates(previousTemplates); + console.warn('meta-bind | failed to save input field templates', errors); + new Notice( + `meta-bind | Input field template could not be saved: ${errors[0]?.message ?? 'Unknown validation error.'}`, + ); + return false; + } + + this.mb.updateSettings(settings => { + settings.inputFieldTemplates = templates; + }); + + return true; + } + + private openInputFieldTemplateModal( + template: InputFieldTemplate | undefined, + onSubmit: (template: InputFieldTemplate) => boolean | void, + ): void { + new InputFieldTemplateSettingModal(this.app, template, onSubmit).open(); + } +} diff --git a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingComponent.svelte b/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingComponent.svelte deleted file mode 100644 index ce09f937..00000000 --- a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingComponent.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - -
-

Meta Bind Input Field Templates

- - {#each inputFieldTemplates as template} - - {/each} - - - - {#if errorCollection} -
-

Some Templates Failed to Parse

- - -
- {/if} - - - - - -
diff --git a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingModal.ts b/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingModal.ts deleted file mode 100644 index 97090e80..00000000 --- a/packages/obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingModal.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { InputFieldTemplate } from 'meta-bind-core/src/Settings'; -import type { ErrorCollection } from 'meta-bind-core/src/utils/errors/ErrorCollection'; -import type { ObsMetaBind } from 'meta-bind-obsidian/src/ObsMB'; -import type { App } from 'obsidian'; -import { Modal } from 'obsidian'; -import type { Component as SvelteComponent } from 'svelte'; -import { mount, unmount } from 'svelte'; -import InputFieldTemplatesSettingComponent from 'meta-bind-obsidian/src/settings/inputFieldTemplateSetting/InputFieldTemplatesSettingComponent.svelte'; - -export class InputFieldTemplatesSettingModal extends Modal { - readonly mb: ObsMetaBind; - private component: ReturnType | undefined; - - constructor(app: App, mb: ObsMetaBind) { - super(app); - this.mb = mb; - } - - public onOpen(): void { - this.contentEl.empty(); - if (this.component) { - void unmount(this.component); - } - - this.component = mount(InputFieldTemplatesSettingComponent, { - target: this.contentEl, - props: { - inputFieldTemplates: structuredClone(this.mb.getSettings().inputFieldTemplates), - modal: this, - }, - }); - } - - public onClose(): void { - this.contentEl.empty(); - if (this.component) { - void unmount(this.component); - } - } - - public save(templates: InputFieldTemplate[]): ErrorCollection | undefined { - const errorCollection = this.mb.inputFieldParser.parseTemplates(templates); - if (errorCollection.hasErrors()) { - return errorCollection; - } - - this.mb.updateSettings(settings => { - settings.inputFieldTemplates = templates; - }); - - return undefined; - } -} diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index cca20605..1d26a55b 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -772,20 +772,6 @@ code.mb-warning { white-space: pre; } -.mb-excluded-folders-table-input-cell { - width: 100%; -} - -.mb-excluded-folders-table-input-cell > input { - width: 100%; -} - -.mb-textarea { - width: 100%; - height: 100px; - resize: vertical; -} - .mb-icon-wrapper { display: block; position: relative; @@ -882,6 +868,12 @@ div.setting-item > div.setting-item-control.mb-vertical-control { width: 100%; } +.mb-settings-list-modal-textarea { + width: 100%; + min-height: 120px; + resize: vertical; +} + .mb-search-modal-element-description { color: var(--text-faint); }