Skip to content

Commit b495086

Browse files
committed
metadata button action
1 parent 7da3a99 commit b495086

File tree

13 files changed

+203
-45
lines changed

13 files changed

+203
-45
lines changed

exampleVault/Button Example.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
---
2+
count: 3
3+
---
14
Meta Bind is getting Buttons
25

36
text `BUTTON[docs-button]` text
@@ -153,6 +156,48 @@ actions:
153156
154157
```
155158

159+
### Modifying Front-matter
160+
161+
```meta-bind-button
162+
label: "+1"
163+
hidden: true
164+
id: "count-increment"
165+
style: default
166+
actions:
167+
- type: updateMetadata
168+
bindTarget: count
169+
evaluate: true
170+
value: x + 1
171+
172+
```
173+
174+
```meta-bind-button
175+
label: "-1"
176+
hidden: true
177+
id: "count-decrement"
178+
style: default
179+
actions:
180+
- type: updateMetadata
181+
bindTarget: count
182+
evaluate: true
183+
value: x - 1
184+
185+
```
186+
187+
```meta-bind-button
188+
label: "Reset"
189+
hidden: true
190+
id: "count-reset"
191+
style: default
192+
actions:
193+
- type: updateMetadata
194+
bindTarget: count
195+
evaluate: false
196+
value: "0"
197+
198+
```
199+
200+
`BUTTON[count-decrement, count-reset, count-increment]` `VIEW[{count}]`
156201

157202
## Invalid Buttons
158203

@@ -167,4 +212,4 @@ actions:
167212
- type: command
168213
command: obsidian-meta-bind-plugin:open-help
169214
170-
```
215+
```

src/config/ButtonConfig.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum ButtonActionType {
1212
INPUT = 'input',
1313
SLEEP = 'sleep',
1414
TEMPLATER_CREATE_NOTE = 'templaterCreateNote',
15+
UPDATE_METADATA = 'updateMetadata',
1516
}
1617

1718
export interface CommandButtonAction {
@@ -47,13 +48,21 @@ export interface TemplaterCreateNoteButtonAction {
4748
openNote?: boolean;
4849
}
4950

51+
export interface UpdateMetadataButtonAction {
52+
type: ButtonActionType.UPDATE_METADATA;
53+
bindTarget: string;
54+
evaluate: boolean;
55+
value: string;
56+
}
57+
5058
export type ButtonAction =
5159
| CommandButtonAction
5260
| JSButtonAction
5361
| OpenButtonAction
5462
| InputButtonAction
5563
| SleepButtonAction
56-
| TemplaterCreateNoteButtonAction;
64+
| TemplaterCreateNoteButtonAction
65+
| UpdateMetadataButtonAction;
5766

5867
export interface ButtonConfig {
5968
label: string;

src/config/ButtonConfigValidators.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,45 @@ import {
1111
type OpenButtonAction,
1212
type SleepButtonAction,
1313
type TemplaterCreateNoteButtonAction,
14+
type UpdateMetadataButtonAction,
1415
} from './ButtonConfig';
1516

16-
export const ButtonConfigValidators = schemaForType<CommandButtonAction>()(
17+
export const V_CommandButtonAction = schemaForType<CommandButtonAction>()(
1718
z.object({
1819
type: z.literal(ButtonActionType.COMMAND),
1920
command: z.string(),
2021
}),
2122
);
22-
export const JSButtonActionValidator = schemaForType<JSButtonAction>()(
23+
24+
export const V_JSButtonAction = schemaForType<JSButtonAction>()(
2325
z.object({
2426
type: z.literal(ButtonActionType.JS),
2527
file: z.string(),
2628
}),
2729
);
28-
export const OpenButtonActionValidator = schemaForType<OpenButtonAction>()(
30+
31+
export const V_OpenButtonAction = schemaForType<OpenButtonAction>()(
2932
z.object({
3033
type: z.literal(ButtonActionType.OPEN),
3134
link: z.string(),
3235
}),
3336
);
34-
export const InputButtonActionValidator = schemaForType<InputButtonAction>()(
37+
38+
export const V_InputButtonAction = schemaForType<InputButtonAction>()(
3539
z.object({
3640
type: z.literal(ButtonActionType.INPUT),
3741
str: z.string(),
3842
}),
3943
);
40-
export const SleepButtonActionValidator = schemaForType<SleepButtonAction>()(
44+
45+
export const V_SleepButtonAction = schemaForType<SleepButtonAction>()(
4146
z.object({
4247
type: z.literal(ButtonActionType.SLEEP),
4348
ms: z.number(),
4449
}),
4550
);
46-
export const TemplaterCreateNoteButtonActionValidator = schemaForType<TemplaterCreateNoteButtonAction>()(
51+
52+
export const V_TemplaterCreateNoteButtonAction = schemaForType<TemplaterCreateNoteButtonAction>()(
4753
z.object({
4854
type: z.literal(ButtonActionType.TEMPLATER_CREATE_NOTE),
4955
templateFile: z.string(),
@@ -52,28 +58,40 @@ export const TemplaterCreateNoteButtonActionValidator = schemaForType<TemplaterC
5258
openNote: z.boolean().optional(),
5359
}),
5460
);
55-
export const ButtonActionValidator = schemaForType<ButtonAction>()(
61+
export const V_UpdateMetadataButtonAction = schemaForType<UpdateMetadataButtonAction>()(
62+
z.object({
63+
type: z.literal(ButtonActionType.UPDATE_METADATA),
64+
bindTarget: z.coerce.string(),
65+
evaluate: z.boolean(),
66+
value: z.coerce.string(),
67+
}),
68+
);
69+
70+
export const V_ButtonAction = schemaForType<ButtonAction>()(
5671
z.union([
57-
ButtonConfigValidators,
58-
JSButtonActionValidator,
59-
OpenButtonActionValidator,
60-
InputButtonActionValidator,
61-
SleepButtonActionValidator,
62-
TemplaterCreateNoteButtonActionValidator,
72+
V_CommandButtonAction,
73+
V_JSButtonAction,
74+
V_OpenButtonAction,
75+
V_InputButtonAction,
76+
V_SleepButtonAction,
77+
V_TemplaterCreateNoteButtonAction,
78+
V_UpdateMetadataButtonAction,
6379
]),
6480
);
65-
export const ButtonStyleValidator = z.nativeEnum(ButtonStyleType);
66-
export const ButtonConfigValidator = schemaForType<ButtonConfig>()(
81+
82+
export const V_ButtonStyleType = z.nativeEnum(ButtonStyleType);
83+
84+
export const V_ButtonConfig = schemaForType<ButtonConfig>()(
6785
z
6886
.object({
6987
label: z.string(),
70-
style: ButtonStyleValidator,
88+
style: V_ButtonStyleType,
7189
class: z.string().optional(),
7290
tooltip: z.string().optional(),
7391
id: z.string().optional(),
7492
hidden: z.boolean().optional(),
75-
action: ButtonActionValidator.optional(),
76-
actions: ButtonActionValidator.array().optional(),
93+
action: V_ButtonAction.optional(),
94+
actions: V_ButtonAction.array().optional(),
7795
})
7896
.superRefine(oneOf('action', 'actions')),
7997
);

src/fields/button/ButtonActionRunner.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import {
88
type OpenButtonAction,
99
type SleepButtonAction,
1010
type TemplaterCreateNoteButtonAction,
11+
type UpdateMetadataButtonAction,
1112
} from '../../config/ButtonConfig';
1213
import { MDLinkParser } from '../../parsers/MarkdownLinkParser';
1314
import { type IPlugin } from '../../IPlugin';
14-
import { openURL } from '../../utils/Utils';
15+
import { getUUID, openURL } from '../../utils/Utils';
16+
import { Signal } from '../../utils/Signal';
1517

1618
export class ButtonActionRunner {
1719
plugin: IPlugin;
@@ -58,6 +60,13 @@ export class ButtonActionRunner {
5860
fileName: '',
5961
openNote: true,
6062
} satisfies TemplaterCreateNoteButtonAction;
63+
} else if (type === ButtonActionType.UPDATE_METADATA) {
64+
return {
65+
type: ButtonActionType.UPDATE_METADATA,
66+
bindTarget: '',
67+
evaluate: false,
68+
value: '',
69+
} satisfies UpdateMetadataButtonAction;
6170
}
6271

6372
throw new Error(`Unknown button action type: ${type}`);
@@ -82,6 +91,9 @@ export class ButtonActionRunner {
8291
} else if (action.type === ButtonActionType.TEMPLATER_CREATE_NOTE) {
8392
await this.runTemplaterCreateNoteAction(action);
8493
return;
94+
} else if (action.type === ButtonActionType.UPDATE_METADATA) {
95+
await this.runUpdateMetadataAction(action, filePath);
96+
return;
8597
}
8698

8799
throw new Error(`Unknown button action type`);
@@ -120,4 +132,16 @@ export class ButtonActionRunner {
120132
async runTemplaterCreateNoteAction(_action: TemplaterCreateNoteButtonAction): Promise<void> {
121133
throw new Error('Not supported');
122134
}
135+
136+
async runUpdateMetadataAction(action: UpdateMetadataButtonAction, filePath: string): Promise<void> {
137+
const bindTarget = this.plugin.api.bindTargetParser.parseAndValidateBindTarget(action.bindTarget, filePath);
138+
const uuid = getUUID();
139+
const signal = new Signal<unknown>(undefined);
140+
const subscription = this.plugin.metadataManager.subscribe(uuid, signal, bindTarget, () => {});
141+
subscription.applyUpdate({
142+
value: action.value,
143+
evaluate: action.evaluate,
144+
});
145+
subscription.unsubscribe();
146+
}
123147
}

src/fields/button/ButtonBuilderModalComponent.svelte

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
let buttonConfig: ButtonConfig = {
2525
label: 'This is a button',
2626
hidden: false,
27+
class: '',
28+
tooltip: '',
2729
id: '',
2830
style: ButtonStyleType.DEFAULT,
2931
actions: [],
@@ -92,6 +94,8 @@
9294
return 'Sleep for Some Time';
9395
} else if (actionType === ButtonActionType.TEMPLATER_CREATE_NOTE) {
9496
return 'Create a New Note Using Templater';
97+
} else if (actionType === ButtonActionType.UPDATE_METADATA) {
98+
return 'Update Metadata';
9599
}
96100
97101
return 'CHANGE ME';
@@ -112,6 +116,20 @@
112116
</select>
113117
</SettingComponent>
114118

119+
<SettingComponent
120+
name="CSS Classes"
121+
description="A list of CSS classes to add to the button. Multiple classes should be separated by a space."
122+
>
123+
<input type="text" bind:value={buttonConfig.class} />
124+
</SettingComponent>
125+
126+
<SettingComponent
127+
name="Tooltip"
128+
description="A tooltip to show when hovering the button. If not set, the button label will be shown instead."
129+
>
130+
<input type="text" bind:value={buttonConfig.tooltip} />
131+
</SettingComponent>
132+
115133
<SettingComponent name="ID" description="An ID that allows the button to be referenced in inline buttons.">
116134
<input type="text" bind:value={buttonConfig.id} />
117135
</SettingComponent>
@@ -186,12 +204,7 @@ Add action of type
186204
{/if}
187205

188206
{#if action.type === ButtonActionType.TEMPLATER_CREATE_NOTE}
189-
<p>
190-
Remove Action
191-
<Button variant="destructive" on:click={() => removeAction(i)}>
192-
<Icon iconName="x"></Icon>
193-
</Button>
194-
</p>
207+
<Button variant="destructive" on:click={() => removeAction(i)}>Remove Action</Button>
195208

196209
<SettingComponent
197210
name="Template File: {action.templateFile || 'none'}"
@@ -217,6 +230,22 @@ Add action of type
217230
<Toggle bind:checked={action.openNote}></Toggle>
218231
</SettingComponent>
219232
{/if}
233+
234+
{#if action.type === ButtonActionType.UPDATE_METADATA}
235+
<Button variant="destructive" on:click={() => removeAction(i)}>Remove Action</Button>
236+
237+
<SettingComponent name="Metadata Property" description="The metadata property in form of a bind target.">
238+
<input type="text" bind:value={action.bindTarget} placeholder="some value" />
239+
</SettingComponent>
240+
241+
<SettingComponent name="Value" description="The new value.">
242+
<input type="text" bind:value={action.value} placeholder="some value" />
243+
</SettingComponent>
244+
245+
<SettingComponent name="Evaluate" description="Whether to evaluate the value as a JS expression.">
246+
<Toggle bind:checked={action.evaluate}></Toggle>
247+
</SettingComponent>
248+
{/if}
220249
{/each}
221250

222251
<h4>Preview</h4>

src/fields/button/ButtonField.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { type ButtonConfig } from '../../config/ButtonConfig';
22
import ButtonComponent from '../../utils/components/ButtonComponent.svelte';
33
import { type IPlugin } from '../../IPlugin';
4-
import { ButtonConfigValidator } from '../../config/ButtonConfigValidators';
4+
import { V_ButtonConfig } from '../../config/ButtonConfigValidators';
55
import { ErrorLevel, MetaBindButtonError } from '../../utils/errors/MetaBindErrors';
66
import { DocsUtils } from '../../utils/DocsUtils';
7+
import { isTruthy } from '../../utils/Utils';
78

89
export class ButtonField {
910
plugin: IPlugin;
@@ -33,7 +34,7 @@ export class ButtonField {
3334
props: {
3435
variant: config.style,
3536
label: config.label,
36-
tooltip: config.tooltip ?? config.label,
37+
tooltip: isTruthy(config.tooltip) ? config.tooltip : config.label,
3738
onClick: async (): Promise<void> => {
3839
await this.plugin.api.buttonActionRunner.runButtonAction(config, this.filePath);
3940
},
@@ -44,7 +45,7 @@ export class ButtonField {
4445
public mount(container: HTMLElement): void {
4546
container.empty();
4647
container.addClass('mb-button', this.inline ? 'mb-button-inline' : 'mb-button-block');
47-
const validationResult = ButtonConfigValidator.safeParse(this.unvalidatedConfig);
48+
const validationResult = V_ButtonConfig.safeParse(this.unvalidatedConfig);
4849

4950
if (!validationResult.success) {
5051
throw new MetaBindButtonError({
@@ -68,7 +69,7 @@ export class ButtonField {
6869
}
6970

7071
if (this.config.class) {
71-
container.addClass(...this.config.class.split(' '));
72+
container.addClass(...this.config.class.split(' ').filter(x => x !== ''));
7273
}
7374

7475
this.renderButton(container);

src/fields/inputFields/AbstractInputField.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export abstract class AbstractInputField<MetadataValueType, ComponentValueType>
156156
}
157157

158158
public unmount(): void {
159-
this.computedSignal.listeners = [];
159+
this.computedSignal.unregisterAllListeners();
160160
this.metadataSubscription?.unsubscribe();
161161

162162
this.inputFieldComponent.unmount();

src/fields/inputFields/InputFieldComponent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class InputFieldComponent<Value> extends Notifier<Value, Listener<Value>>
5555
* This unmounts the component.
5656
*/
5757
public unmount(): void {
58-
this.listeners = [];
58+
this.unregisterAllListeners();
5959
this.svelteComponentInstance?.$destroy();
6060

6161
this.mounted = false;

0 commit comments

Comments
 (0)