Skip to content

Commit a6bd331

Browse files
committed
prompt API
1 parent 73750d0 commit a6bd331

File tree

11 files changed

+331
-11
lines changed

11 files changed

+331
-11
lines changed

bun.lockb

1.91 KB
Binary file not shown.

exampleVault/promptTest.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// const ret = await engine.prompt.button({
2+
// title: 'Test Prompt',
3+
// buttons: [
4+
// {
5+
// label: 'True',
6+
// value: true,
7+
// },
8+
// {
9+
// label: 'False',
10+
// value: false,
11+
// },
12+
// {
13+
// label: 'Cancel',
14+
// value: undefined,
15+
// }
16+
// ]
17+
// });
18+
19+
// const ret = await engine.prompt.yesNo({
20+
// title: 'Is this a test?',
21+
// content: 'Are you sure this is a test? Are you sure that your choice is really meaningless?',
22+
// });
23+
24+
25+
const files = engine.query.files((file) => {
26+
return {
27+
label: file.name,
28+
value: file.pat,
29+
};
30+
});
31+
32+
const ret = await engine.prompt.suggester({
33+
placeholder: 'Select a file',
34+
options: files,
35+
});
36+
37+
console.log(ret);

jsEngine/api/API.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { LibAPI } from 'jsEngine/api/LibAPI';
88
import { type JsFunc } from 'jsEngine/engine/JsExecution';
99
import { InternalAPI } from 'jsEngine/api/Internal';
1010
import { QueryAPI } from 'jsEngine/api/QueryAPI';
11+
import { PromptAPI } from 'jsEngine/api/PromptAPI';
1112

1213
export class API {
1314
/**
@@ -35,6 +36,7 @@ export class API {
3536
* API to query your vault with simple javascript functions.
3637
*/
3738
readonly query: QueryAPI;
39+
readonly prompt: PromptAPI;
3840
/**
3941
* API to interact with js engines internals.
4042
*/
@@ -49,6 +51,7 @@ export class API {
4951
this.message = new MessageAPI(this);
5052
this.lib = new LibAPI(this);
5153
this.query = new QueryAPI(this);
54+
this.prompt = new PromptAPI(this);
5255
this.internal = new InternalAPI(this);
5356
}
5457

jsEngine/api/PromptAPI.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import type { API } from 'jsEngine/api/API';
2+
import { ButtonStyleType } from 'jsEngine/utils/Util';
3+
import { SvelteModal } from 'jsEngine/api/prompts/SvelteModal';
4+
import { mount } from 'svelte';
5+
import ButtonModalComponent from 'jsEngine/api/prompts/ButtonModalComponent.svelte';
6+
import { Suggester } from 'jsEngine/api/prompts/Suggester';
7+
8+
export interface ModalPromptOptions {
9+
/**
10+
* The title of the modal.
11+
*/
12+
title: string;
13+
/**
14+
* A list of CSS classes to apply to the modal.
15+
*/
16+
classes?: string[];
17+
}
18+
19+
export interface ButtonPromptOptions<T> extends ModalPromptOptions {
20+
/**
21+
* Text content to display in the modal.
22+
*/
23+
content?: string;
24+
/**
25+
* A list of buttons to display in the modal.
26+
*/
27+
buttons: {
28+
label: string;
29+
value: T;
30+
variant?: ButtonStyleType;
31+
}[];
32+
}
33+
34+
export interface ConfirmPromptOptions extends ModalPromptOptions {
35+
/**
36+
* Text content to display in the modal.
37+
*/
38+
content?: string;
39+
}
40+
41+
export interface YesNoPromptOptions extends ModalPromptOptions {
42+
/**
43+
* Text content to display in the modal.
44+
*/
45+
content?: string;
46+
}
47+
48+
export interface SuggesterPromptOptions<T> {
49+
placeholder?: string;
50+
options: SuggesterOption<T>[];
51+
}
52+
53+
export interface SuggesterOption<T> {
54+
value: T;
55+
label: string;
56+
}
57+
58+
export class PromptAPI {
59+
readonly apiInstance: API;
60+
61+
constructor(apiInstance: API) {
62+
this.apiInstance = apiInstance;
63+
}
64+
65+
/**
66+
* Prompts the user with a modal containing a list of buttons.
67+
* Returns the value of the button that was clicked, or undefined if the modal was closed.
68+
*/
69+
public button<T>(options: ButtonPromptOptions<T>): Promise<T | undefined> {
70+
return new Promise<T | undefined>((resolve, reject) => {
71+
try {
72+
new SvelteModal(
73+
this.apiInstance.app,
74+
options,
75+
(modal, targetEl) => {
76+
return mount(ButtonModalComponent, {
77+
target: targetEl,
78+
props: {
79+
options,
80+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
81+
modal: modal as any,
82+
},
83+
});
84+
},
85+
// we cast to narrow the type, so that it is inferred correctly
86+
resolve as (value: T | undefined) => void,
87+
).open();
88+
} catch (error) {
89+
reject(error);
90+
}
91+
});
92+
}
93+
94+
/**
95+
* Prompts the user with a confirm/cancel dialog.
96+
* Returns true if the user confirms, false if the user cancels or otherwise closes the modal.
97+
*/
98+
public async confirm(options: ConfirmPromptOptions): Promise<boolean> {
99+
return (
100+
(await this.button<boolean>({
101+
...options,
102+
buttons: [
103+
{
104+
label: 'Confirm',
105+
value: true,
106+
variant: ButtonStyleType.PRIMARY,
107+
},
108+
{
109+
label: 'Cancel',
110+
value: false,
111+
variant: ButtonStyleType.DEFAULT,
112+
},
113+
],
114+
})) ?? false
115+
);
116+
}
117+
118+
/**
119+
* Prompts the user with a yes/no dialog.
120+
* Returns true if the user selects yes, false if the user selects no, and undefined if the user otherwise closes the modal.
121+
*/
122+
public async yesNo(options: YesNoPromptOptions): Promise<boolean | undefined> {
123+
return await this.button<boolean>({
124+
...options,
125+
buttons: [
126+
{
127+
label: 'Yes',
128+
value: true,
129+
variant: ButtonStyleType.PRIMARY,
130+
},
131+
{
132+
label: 'No',
133+
value: false,
134+
variant: ButtonStyleType.DEFAULT,
135+
},
136+
],
137+
});
138+
}
139+
140+
/**
141+
* Prompts the user with a fuzzy finder suggester dialog.
142+
* Returns the value of the selected option, or undefined if the user closes the modal.
143+
*/
144+
public suggester<T>(options: SuggesterPromptOptions<T>): Promise<T | undefined> {
145+
return new Promise<T | undefined>((resolve, reject) => {
146+
try {
147+
new Suggester<T>(this.apiInstance.app, options, resolve).open();
148+
} catch (error) {
149+
reject(error);
150+
}
151+
});
152+
}
153+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script lang="ts" generics="T">
2+
import type { ButtonPromptOptions } from 'jsEngine/api/PromptAPI';
3+
import Button from 'jsEngine/utils/Button.svelte';
4+
import type { SvelteModal } from './SvelteModal';
5+
import { ButtonStyleType } from 'jsEngine/utils/Util';
6+
7+
const {
8+
options,
9+
modal,
10+
}: {
11+
options: ButtonPromptOptions<T>;
12+
modal: SvelteModal<any, T>;
13+
} = $props();
14+
</script>
15+
16+
<p>{options.content}</p>
17+
18+
<div class="modal-button-container">
19+
{#each options.buttons as button}
20+
<Button variant={button.variant ?? ButtonStyleType.DEFAULT} onclick={() => modal.submit(button.value)}>
21+
{button.label}
22+
</Button>
23+
{/each}
24+
</div>

jsEngine/api/prompts/Suggester.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { type App, FuzzySuggestModal } from 'obsidian';
2+
import type { SuggesterOption, SuggesterPromptOptions } from 'jsEngine/api/PromptAPI';
3+
4+
export class Suggester<T> extends FuzzySuggestModal<SuggesterOption<T>> {
5+
private readonly options: SuggesterPromptOptions<T>;
6+
private readonly onSubmit: (value: T | undefined) => void;
7+
private selectedValue: T | undefined;
8+
9+
constructor(app: App, options: SuggesterPromptOptions<T>, onSubmit: (value: T | undefined) => void) {
10+
super(app);
11+
12+
this.options = options;
13+
this.onSubmit = onSubmit;
14+
15+
if (options.placeholder) {
16+
this.setPlaceholder(options.placeholder);
17+
}
18+
}
19+
20+
getItems(): SuggesterOption<T>[] {
21+
return this.options.options;
22+
}
23+
24+
getItemText(item: SuggesterOption<T>): string {
25+
return item.label;
26+
}
27+
28+
onChooseItem(item: SuggesterOption<T>, _: MouseEvent | KeyboardEvent): void {
29+
this.selectedValue = item.value;
30+
}
31+
32+
onOpen(): void {
33+
super.onOpen();
34+
35+
this.selectedValue = undefined;
36+
}
37+
38+
onClose(): void {
39+
super.onClose();
40+
41+
queueMicrotask(() => {
42+
this.onSubmit(this.selectedValue);
43+
});
44+
}
45+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { type App, Modal } from 'obsidian';
2+
import { type Component as SvelteComponent, unmount } from 'svelte';
3+
import type { ModalPromptOptions } from 'jsEngine/api/PromptAPI';
4+
5+
export class SvelteModal<Component extends SvelteComponent, T> extends Modal {
6+
private component: ReturnType<Component> | undefined;
7+
private readonly options: ModalPromptOptions;
8+
private readonly createComponent: (modal: SvelteModal<Component, T>, targetEl: HTMLElement) => ReturnType<Component>;
9+
private readonly onSubmit: (value: T | undefined) => void;
10+
private submitted: boolean;
11+
12+
constructor(
13+
app: App,
14+
options: ModalPromptOptions,
15+
createComponent: (modal: SvelteModal<Component, T>, targetEl: HTMLElement) => ReturnType<Component>,
16+
onSubmit: (value: T | undefined) => void,
17+
) {
18+
super(app);
19+
20+
this.options = options;
21+
this.createComponent = createComponent;
22+
this.onSubmit = onSubmit;
23+
24+
this.submitted = false;
25+
}
26+
27+
public onOpen(): void {
28+
this.submitted = false;
29+
30+
this.setTitle(this.options.title);
31+
this.containerEl.addClasses(this.options.classes ?? []);
32+
33+
this.createComponent(this, this.contentEl);
34+
}
35+
36+
public onClose(): void {
37+
if (!this.submitted) {
38+
this.submitted = true;
39+
this.onSubmit(undefined);
40+
}
41+
42+
if (this.component) {
43+
unmount(this.component);
44+
}
45+
this.contentEl.empty();
46+
}
47+
48+
public submit(value: T): void {
49+
this.submitted = true;
50+
this.onSubmit(value);
51+
this.close();
52+
}
53+
}

jsEngine/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export default class JsEnginePlugin extends Plugin {
4343
},
4444
});
4545

46+
this.app.workspace;
47+
4648
await this.registerCodeMirrorMode();
4749
}
4850

0 commit comments

Comments
 (0)