The topic about interactive programming and mini apps inside Logseq seems super exciting nowadays.
Here is a quick way to execute JavaScript code, which is located inside Markdown codeblocks.
Thanks to @mentaloid for work and inspiration in Edit and run javascript code inside Logseq itself. My requirements are a bit simpler - just execute that code block with a button. If you are seeking for Jupyter-like notebook experience and/or more programming languages support, above topic might be better suited! Go credit him as well, if you like the overall topic.
Specific goals:
- vanilla Logseq - no plugin
- performance - no need to watch DOM with
MutationObserver
- convention-driven: Place code block as first child block under code button
- encapsulation: based on official Web components standard, mini app in Logseq
Getting started
1. Put code for the CodeButton
web component (Attachment 1) in custom.js
. Then create a macro for ease of use:
:macros {
:button "<div is='code-button' name=$1 class=$2></div>"
}
1a. Restart.
2. Write in a block content {{button Click}}
.
3. Create child block with content
```js logseq.api.show_msg("Hello world", "info"); ```
4. Get greeting like in above picture.
More examples
// Insert childblock (current block ID of codeblock available via this.uuid)
// this.target_uuid has ID of block containing the button.
// this.ev is the fired click event.
logseq.api.insert_block(this.uuid, "new kid on the block")
// simple query
const results = logseq.api.q("[[MyPage]]");
console.log(results);
// advanced query
const results = logseq.api.datascript_query(`[
:find (pull ?b[*])
:where
[?p :block/name "mypage"]
[?b :block/refs ?p]]`);
console.log(results);
Invoking the macro with quiet
{{button "Simple query",quiet}}
will suppress code execution popup.
Notes
- To save you an app crash with
logseq.api.show_msg
: Don’t feed it with complex objects. Useconsole.log
or write blocks instead. In other words: e.g. no.logseq.api.show_msg(this.ev, "info");
- TODO: Post about web components in Logseq as mini app, current limitations in Logseq
Attachment 1
class CodeButton extends HTMLDivElement {
constructor() {
super();
const style = document.createElement("style");
style.textContent = `
button.button-style {
display: inline-block;
outline: none;
cursor: pointer;
padding: 0 10px;
background-color: #fff;
border-radius: 0.25rem;
border: 1px solid #0070d2;
color: #0070d2;
font-size: 13px;
line-height: 30px;
font-weight: 400;
text-align: center;
user-select: none;
}
button.button-style::before {
content: "➤ ";
}
button.button-style:hover {
background-color: #f4f6f9;
}`;
this.appendChild(style);
this.insertAdjacentHTML(
"beforeend",
`<button class="button-style">${this.getAttribute("name") || "Click"}</button>`,
);
}
code_from_childblock(ele) {
const ele_block_uuid = ele
.closest("[id^='block-content-'][blockid]")
?.getAttribute("blockid");
if (!ele_block_uuid) return;
// Convention: Codeblock is first child of button block.
const first_child_block_uuid =
logseq.api.get_block(ele_block_uuid)?.children?.[0]?.[1];
if (!first_child_block_uuid) return;
const content = logseq.api.get_block(first_child_block_uuid)?.content;
if (!content) return;
const regex =
/(?<![\r\n])^```(?:javascript|js)\n(.*)\n```(?=[\r\n]?$(?![\r\n]))/msu;
const code = content.match(regex)?.[1];
if (!code) return;
return {
target_uuid: ele_block_uuid,
uuid: first_child_block_uuid,
code,
};
}
handleClick(ev) {
const AsyncFunction = async function () {}.constructor;
const { code, ...rest } = this.code_from_childblock(ev.target) ?? {};
if (!code) {
logseq.api.show_msg("No code block found", "error", { timeout: 5000 });
return;
}
try {
AsyncFunction('"use strict";' + code).call({ ev, ...rest });
if (!this.classList.contains("quiet"))
logseq.api.show_msg("Executed code", "success", { timeout: 2000 });
} catch (er) {
logseq.api.show_msg(`Code error: ${er.message}`, "error", {
timeout: 5000,
});
throw er;
}
}
connectedCallback() {
this.querySelector("button").addEventListener(
"click",
this.handleClick.bind(this),
);
}
}
customElements.define("code-button", CodeButton, { extends: "div" });