Skip to content

Commit

Permalink
feat(form-runtime): markdown block input
Browse files Browse the repository at this point in the history
fixes [Feature request]Image / Instruction panel  #327
  • Loading branch information
danielo515 committed Sep 29, 2024
1 parent 7e89349 commit 567d391
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 16 deletions.
4 changes: 3 additions & 1 deletion EXAMPLE_VAULT/.obsidian/app.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{}
{
"alwaysUpdateLinks": true
}
41 changes: 40 additions & 1 deletion EXAMPLE_VAULT/.obsidian/plugins/modal-form/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
"isRequired": false,
"input": {
"type": "document_block",
"body": "return `This field contains several hidden fields`"
"body": "return `This field contains several hidden fields ${form.hidden_text}`"
}
},
{
Expand Down Expand Up @@ -264,6 +264,45 @@
}
],
"version": "1"
},
{
"title": "Codeblock examples",
"name": "codeblocks",
"fields": [
{
"name": "md_block",
"label": "",
"description": "",
"isRequired": false,
"input": {
"type": "markdown_block",
"body": "return `# hello\n- line 1\n- ${form.text}\n- ![[image.png]]`"
}
},
{
"name": "text",
"label": "",
"description": "",
"isRequired": false,
"input": {
"type": "text",
"hidden": false
}
},
{
"name": "block_01",
"label": "",
"description": "",
"input": {
"type": "document_block",
"allowUnknownValues": false,
"hidden": false,
"body": "return `${form.text}`"
},
"isRequired": false
}
],
"version": "1"
}
]
}
Binary file added EXAMPLE_VAULT/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/core/formDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const InputTypeReadable: Record<AllFieldTypes, string> = {
dataview: "Dataview",
multiselect: "Multiselect",
document_block: "Document block",
markdown_block: "Markdown block",
} as const;

export function isDataViewSource(input: unknown): input is inputDataviewSource {
Expand Down
13 changes: 13 additions & 0 deletions src/core/input/InputDefinitionSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ const DocumentBlock = object({
body: string(),
});

/**
* Same as DocumentBlock, but with accepts and markdown as result
* from the body function
*/
const MarkdownBlock = object({
type: literal("markdown_block"),
body: string(),
});

// Codec for all the input types
export const InputTypeSchema = union([
InputBasicSchema,
Expand All @@ -143,6 +152,7 @@ export const InputTypeSchema = union([
InputSelectFixedSchema,
MultiselectSchema,
DocumentBlock,
MarkdownBlock,
]);

export type Input = Output<typeof InputTypeSchema>;
Expand All @@ -165,6 +175,7 @@ export const InputTypeToParserMap: Record<AllFieldTypes, ParsingFn<BaseSchema>>
dataview: parseC(InputDataviewSourceSchema),
multiselect: parseC(MultiselectSchema),
document_block: parseC(DocumentBlock),
markdown_block: parseC(MarkdownBlock),
};

//=========== Types derived from schemas
Expand All @@ -180,6 +191,7 @@ export type inputTag = Output<typeof InputTagSchema>;
export type inputType = Output<typeof InputTypeSchema>;

export type DocumentBlock = Output<typeof DocumentBlock>;
export type MarkdownBlock = Output<typeof MarkdownBlock>;

export function requiresListOfStrings(input: inputType): boolean {
const type = input.type;
Expand All @@ -193,6 +205,7 @@ export function requiresListOfStrings(input: inputType): boolean {
case "folder":
case "slider":
case "document_block":
case "markdown_block":
case "number":
case "text":
case "date":
Expand Down
1 change: 1 addition & 0 deletions src/core/input/dependentFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function availableConditionsForInput(input: FieldDefinition["input"]): Co
case "tag":
case "dataview":
case "document_block":
case "markdown_block":
return [];
default:
return absurd(input);
Expand Down
10 changes: 8 additions & 2 deletions src/views/FormBuilder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,14 @@
bind:value={field.input.query}
{app}
/>
{:else if field.input.type === "document_block"}
<InputBuilderDocumentBlock {index} bind:body={field.input.body} />
{:else if field.input.type === "document_block" || field.input.type === "markdown_block"}
<InputBuilderDocumentBlock
{index}
bind:body={field.input.body}
flavour={field.input.type === "document_block"
? "html"
: "markdown"}
/>
{/if}
</div>

Expand Down
13 changes: 9 additions & 4 deletions src/views/components/Form/DocumentBlock.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { parseFunctionBody, pipe } from "@std";
import * as R from "fp-ts/Record";
import * as TE from "fp-ts/TaskEither";
import { sanitizeHTMLToDom } from "obsidian";
import { App, sanitizeHTMLToDom } from "obsidian";
import { input } from "src/core";
import { FieldValue, FormEngine } from "src/store/formStore";
import { notifyError } from "src/utils/Log";
Expand All @@ -11,7 +11,12 @@
export let form: FormEngine;
export let field: input.DocumentBlock;
$: functionParsed = parseFunctionBody<[Record<string, FieldValue>], string>(field.body, "form");
export let app: App;
$: dv = app.plugins.plugins.dataview?.api;
$: functionParsed = parseFunctionBody<
[Record<string, FieldValue>, unknown, HTMLElement],
string
>(field.body, "form", "dv", "el");
/* I probably... probably should better export the real type the FormEngine has rather than this mess of types... but I wanted to keep that private to the file...
You can argue that I am exposing it with all tis dark magic, but at least this way it is kept in sync if the type changes?
*/
Expand All @@ -26,15 +31,15 @@
pipe(
form.fields,
R.filterMap((field) => field.value),
fn,
(fields) => fn(fields, dv, parent),
),
),
TE.match(
(error) => {
console.error(error);
notifyError("Error in document block")(String(error));
},
(newText) => parent.setText(sanitizeHTMLToDom(newText)),
(newText) => newText && parent.setText(sanitizeHTMLToDom(newText)),
),
)();
return {
Expand Down
61 changes: 61 additions & 0 deletions src/views/components/Form/MarkdownBlock.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script lang="ts">
import { parseFunctionBody, pipe } from "@std";
import * as R from "fp-ts/Record";
import * as TE from "fp-ts/TaskEither";
import { App, Component, MarkdownRenderer } from "obsidian";
import { input } from "src/core";
import { FieldValue, FormEngine } from "src/store/formStore";
import { notifyError } from "src/utils/Log";
import { onDestroy } from "svelte";
import { Subscriber } from "svelte/store";
type GetSubscription<T> = T extends Subscriber<infer U> ? U : never;
export let form: FormEngine;
export let field: input.MarkdownBlock;
export let app: App;
let component = new Component();
$: dv = app.plugins.plugins.dataview?.api;
$: functionParsed = parseFunctionBody<
[Record<string, FieldValue>, unknown, HTMLElement],
string
>(field.body, "form", "dv", "el");
onDestroy(() => component.unload());
// onMount(() => component.load());
/* I probably... probably should better export the real type the FormEngine has rather than this mess of types... but I wanted to keep that private to the file...
You can argue that I am exposing it with all tis dark magic, but at least this way it is kept in sync if the type changes?
*/
function generateContent(
parent: HTMLElement,
form: GetSubscription<Parameters<FormEngine["subscribe"]>[0]>,
execute = false,
) {
if (execute) {
parent.innerHTML = "";
pipe(
functionParsed,
TE.fromEither,
TE.chainW((fn) =>
pipe(
form.fields,
R.filterMap((field) => field.value),
(fields) => fn(fields, dv, parent),
),
),
TE.match(
(error) => {
console.error(error);
notifyError("Error in markdown block")(String(error));
},
(newText) => MarkdownRenderer.render(app, newText, parent, "/", component),
),
)();
}
return {
update(newForm: GetSubscription<Parameters<FormEngine["subscribe"]>[0]>) {
generateContent(parent, newForm, true);
},
};
}
</script>

<div class="markdown-block" use:generateContent={$form}></div>
9 changes: 6 additions & 3 deletions src/views/components/Form/RenderField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
import { App } from "obsidian";
import { FieldDefinition } from "src/core/formDefinition";
import { FormEngine } from "src/store/formStore";
import { logger as l } from "src/utils/Logger";
import InputField from "src/views/components/Form/InputField.svelte";
import ObsidianInputWrapper from "src/views/components/Form/ObsidianInputWrapper.svelte";
import { derived } from "svelte/store";
import DocumentBlock from "./DocumentBlock.svelte";
import InputDataview from "./InputDataview.svelte";
import InputFolder from "./InputFolder.svelte";
import InputNote from "./InputNote.svelte";
import InputSlider from "./inputSlider.svelte";
import InputTag from "./InputTag.svelte";
import InputTextArea from "./InputTextArea.svelte";
import MarkdownBlock from "./MarkdownBlock.svelte";
import MultiSelectField from "./MultiSelectField.svelte";
import ObsidianSelect from "./ObsidianSelect.svelte";
import ObsidianToggle from "./ObsidianToggle.svelte";
import { logger as l } from "src/utils/Logger";
import InputSlider from "./inputSlider.svelte";
export let model: ReturnType<FormEngine["addField"]>;
export let definition: FieldDefinition;
Expand Down Expand Up @@ -54,13 +55,15 @@
<InputNote field={definition} input={definition.input} {value} {errors} {app} />
{:else if definition.input.type === "textarea"}
<InputTextArea field={definition} {value} {errors} />
{:else if definition.input.type === "markdown_block"}
<MarkdownBlock field={definition.input} form={formEngine} {app} />
{:else if definition.input.type === "document_block"}
<!-- I need to put this separated to be able to target the correct slot, it does not work inside #if -->
<ObsidianInputWrapper
label={definition.label || definition.name}
description={definition.description}
>
<DocumentBlock field={definition.input} form={formEngine} slot="info" />
<DocumentBlock field={definition.input} form={formEngine} slot="info" {app} />
</ObsidianInputWrapper>
{:else}
<ObsidianInputWrapper
Expand Down
29 changes: 24 additions & 5 deletions src/views/components/InputBuilderDocumentBlock.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script lang="ts">
import { E, pipe, parseFunctionBody } from "@std";
import { E, parseFunctionBody, pipe } from "@std";
import FormRow from "./FormRow.svelte";
export let body = "";
export let index: number;
export let flavour: "markdown" | "html";
$: id = "document_block_" + index;
const placeholder = "return `Hello ${form.name}!`";
$: errors = pipe(
Expand All @@ -18,12 +19,20 @@

<FormRow label="Document block" {id}>
<span class="modal-form-hint">
This is a document block input. It is not meant to be used as a normal
input, instead it is to render some instructions to the user. It is
expected to be a function body that returns a string. Within the
function body, you can access the form data using the <code>form</code>
{#if flavour === "markdown"}
This is markdown block. It is not a real input, it is used to render some markdown
inside your form.
{:else}
This is a document block input. It is not meant to be used as a normal input, instead it
is to render some instructions to the user.
{/if}
It is expected to contain a function body that returns a string. Within the function body, you
can access the form data using the <code>form</code>
variable. For example:
<pre class="language-js">{placeholder}</pre>
<p>
You also have access to the Dataview API through the <code>dv</code> variable.
</p>
<textarea
bind:value={body}
name="document_block"
Expand All @@ -37,3 +46,13 @@
{/if}
</span></FormRow
>

<style>
code {
border: 1px solid var(--divider-color);
padding: 2px 4px;
}
textarea {
width: 100%;
}
</style>

0 comments on commit 567d391

Please sign in to comment.