Zettelkasten Forum


Offer a more complex plug-in UI with embedded HTML files

edited November 19 in The Archive

Problem: app.prompt() only works for single-line text input. Using the prompt to create long-form text doesn't work well because the app 'freezes' while the prompt is displayed, so you can't use your notes to assemble the text.

From my perspective as the app's developer, writing a tool for you to create a custom user interface would be a lot of work and very error-prone.

Solution: Instead, we can leverage the web browser to render complex forms with well-known standard form elements, and pass the data to plug-ins via the thearchive:// URL scheme.

How the URL scheme works

The URL scheme has this form:

thearchive://plugin/PLUGIN_ID/run?var1=value1&var2=value2
  • PLUGIN_ID is a placeholder for your plug-in's ID.
  • ?var1=value1&var2=value2 is the query parameter of a URL; it's optional.

The query parameter list will be passed to the plug-in during execution via the new (as of b257 2024-11-16) app.env object. In your code, you could access these two query parameters as:

const var1 = app.env["var1"];
const var2 = app.env["var2"];

⚠️ Warning: Plug-ins can always also be started from the app, so app.env will be empty. You need to write your plug-ins to be as defensive as possible and ideally offer either hard-coded default values or prompt the user for input.

Plug-in code: Creating a new file with content

The following plug-in main.js expects two query parameters, filename and content:

let filename = app.env["filename"];
let content = app.env["content"];

if (!filename) cancel("query parameter `filename' missing");
if (!content) cancel("query parameter `content' missing");

output.changeFile.filename = filename;
output.changeFile.content = content;

Its manifest does not require any input, but it will produce an output to change a file with a dynamic filename; the manifest.json looks like this:

{
  "appVersion" : "1.8.0",
  "authors" : [ { "name" : "Christian Tietze" } ],
  "dependencies" : [ ],
  "description" : "",
  "identifier" : "de.zettelkasten.url-demo",
  "input" : {
    "notes" : [],
    "pasteboard" : false,
    "text" : []
  },
  "output" : {
    "changeFile" : {
      "programmaticFilename" : true
    },
    "onCompletion" : "showFile"
  },
  "releaseDate" : "2024-11-15",
  "title" : "URL Demo",
  "version" : "1.0.0"
}

Once enabled, it can run via e.g.:

thearchive://plugin/de.zettelkasten.url-demo/run?filename=New%20File&content=Wow

that would create New File.md with the content "Wow".

These URL's are not intended to be assembled manually, though. This approach shines when combined with automation tools.

HTML form example and template

This basic HTML form contains a filename and content text box, and will invoke thearchive://plugin/de.zettelkasten.url-demo/run to run my test plug-in with the ID "de.zettelkasten.url-demo", passing the filename and content variables as query parameters:

<form action="thearchive://plugin/de.zettelkasten.url-demo/run" method="GET">
  <label for="filename">Filename:</label>
  <input id="filename" name="filename" type="text" value="" required/><br>
  <label for="content">Content:</label>
  <textarea cols="30" id="content" name="content" rows="10" required></textarea><br>
  <input type="submit" value="Create"/>
</form>
<script type="text/javascript">
 let form = document.querySelector("form");
 form.addEventListener("formdata", (e) => {
   const formData = e.formData;
   for (let [key, value] of formData.entries()) {
     formData.set(key, encodeURIComponent(value));
   }
 });
</script>

Sadly, we need to include some JavaScript in the otherwise very simple HTML file to encode spaces in text not as + but %20. Read this for details if you're interested: https://christiantietze.de/posts/2024/11/percent-encode-html-form-values-spaces/

With this setup, the result of submitting the form will be a URL scheme request like so, appending the variables as query parameters:

thearchive://plugin/de.zettelkasten.url-demo/run?filename=The%20Filename&content=Wow

All ugly pieces in action:

Embedding HTML files in the bundle

You can add anything to a .thearchiveplugin bundle (aka directory). Anything but main.js and manifest.json will be irrelevant when it comes to executing plug-ins, though.

But you can bundle HTML files, and then link to them from your plug-in description.

A relative file links in the Markdown description would look like so:

  "description" : "[**Show form**](./form.html) (open the HTML file in your browser)",

... and render as links relative to the plug-in bundle's folder (see tooltip in the picture):

Full HTML example with some basic styling

Here's the full form.html file from my test plug-in with some minimal CSS. It renders like so:

<html>
  <head>
    <meta charset="UTF-8"/>
    <title>Form</title>
    <style>
      html {
        /* via <https://modernfontstacks.com/> */
        font-family: Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif;
        font-weight: normal;
        font-size: clamp(18px, 2vw, 32px);
        line-height: 1.6;
        height: 100vh;
      }
      body {
        margin: 0; padding: 0;
        height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      section {
        padding: 1rem;
        border-radius: 0.5rem;
        background: #eee;
        max-width: 40ch;
      }
      form {
        display: flex;
        flex-direction: column;
      }
      input, textarea {
        font: inherit; font-size: inherit;
        border: 1px solid #444;
      }
      input[type="submit"] {
        background: #444;
        color: #fff;
      }
    </style>
  </head>
  <body>
    <section>
      <form action="thearchive-dev://plugin/de.zettelkasten.url-demo/run" method="GET">
        <label for="filename">Filename:</label>
        <input id="filename" name="filename" type="text" value="" required/><br>
        <label for="content">Content:</label>
        <textarea cols="30" id="content" name="content" rows="10" required></textarea><br>
        <input type="submit" value="Create"/>
      </form>
    </section>
    <script type="text/javascript">
     let form = document.querySelector("form");
     form.addEventListener("formdata", (e) => {
       const formData = e.formData;
       for (let [key, value] of formData.entries()) {
         formData.set(key, encodeURIComponent(value));
       }
     });
    </script>
  </body>
</html>
Post edited by ctietze on

Author at Zettelkasten.de • https://christiantietze.de/

Comments

  • Where to go from here

    Well, that's up to you :) But it's possible to write HTML forms to collect data, e.g. for daily notes for "quantified self"-style sleep quality tracking using a slider (range input) component.

    Or checkboxes and radio buttons.

    You can write a flexible HTML dashboard with multiple forms (think: templates) and run them all through the same minimal plug-in that mostly only needs to deal with processing the query parameters from the app.env collection.

    Author at Zettelkasten.de • https://christiantietze.de/

  • I'm going to try this out. Likely, I misunderstand. I have a couple of questions.

    1. Embedding an HTML form in the plugin bundle is the first step. It looks like the way to call it is for the user to go to "Manage Plug-ins ..." find and select the plug-in in question, and look to the description for a link to launch a form in the browser. If we use the plug-in to gather multiple variables needed to run the plugin, shouldn't the form just appear when the plugin is run? Couldn't we, as plug-in developers, programmatically call the plugin for the user?
    2. Is it possible to output to an HTML form?

    Will Simpson
    My zettelkasten is for my ideas, not the ideas of others. I don’t want to waste my time tinkering with my ZK; I’d rather dive into the work itself. My peak cognition is behind me. One day soon, I will read my last book, write my last note, eat my last meal, and kiss my sweetie for the last time.
    kestrelcreek.com

  • Yeah, currently users need to go to the form manually at least once (and then e.g. copy it somewhere else, create an alias on the desktop, bookmark it, etc.)

    Couldn't we, as plug-in developers, programmatically call the [form] for the user?

    Would be nicer than the current hack, yes. I also tried creating an HTML form in the ./media folder and link to that. While this also works, it requires a note with a link, not much better.

    With future updates, plug-ins will have access to other files in the bundle (e.g. to embed library code). When accessing another JS file safely works, I'll also revisit offering access to other bundle resources like images, HTML files, and such.

    Not yet, though! First release is hacky release :)

    Is it possible to output to an HTML form?

    Not sure; can you give a (made up) example?

    Author at Zettelkasten.de • https://christiantietze.de/

  • An idea that comes to mind to ease opening "attachments" like HTML files from the outside:

    Given that thearchive://plugin/PLUGIN_ID/run is used to execute the plug-in itself, the URL scheme can offer a different action to /run, like /open, and then opening the form can become thearchive://plugin/PLUGIN_ID/open/form.html. Similar with other attachments.

    That doesn't help to bring up the HTML file from running the plug-in, yet, though.

    How one could go about this later: introduce branching execution paths. Show the form when the plug-in is executed directly (i.e. no form data is known from the plug-ins side).

    let filename = app.env["filename"];
    let content = app.env["content"];
    
    if (!filename || !content) {
        // 1) open attachment `form.html`
        // 2) cancel plug-in execution
    }
    
    output.changeFile.filename = filename;
    output.changeFile.content = content;
    

    Author at Zettelkasten.de • https://christiantietze.de/

  • @ctietze said:
    That doesn't help to bring up the HTML file from running the plug-in, yet, though.

    I envision this functionality to be useful in gathering multiple variables at plugin execution.
    1. Variables in the form of selections from a list of templates for new notes.
    2. Multiple checkboxes confirming or denying various run-time options.
    3. In-depth help.

    I envision this functionality to be useful in outputting data from The Archive.
    1. ZK Stats in a pretty format.
    2. Convert a note to a blog post.
    3. Print a note.
    4. Unify several search queries into a single result.

    Will Simpson
    My zettelkasten is for my ideas, not the ideas of others. I don’t want to waste my time tinkering with my ZK; I’d rather dive into the work itself. My peak cognition is behind me. One day soon, I will read my last book, write my last note, eat my last meal, and kiss my sweetie for the last time.
    kestrelcreek.com

Sign In or Register to comment.