Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ When None are configured, nothing happens.

TODONT Mode allows users to archive todos, by replacing the `{{[[TODO]]}}` with a `{{[[ARCHIVED]]}}`. To enable, switch on `icon` in the `TODONT MODE` field in your Roam Depot Settings.

To archive a `TODO`, just hit CMD+SHIFT+ENTER (CTRL in windows). In the text area it inserts `{{[[ARCHIVED]]}}` at the beginning of the block. Any TODOs or DONEs will be replaced with an ARCHIVED. If an ARCHIVED exists, it will be cleared. If none of the above exists, an ARCHIVED is inserted in the block.
To archive a `TODO`, just hit CMD+SHIFT+ENTER (CTRL in windows). In the text area it inserts `{{[[ARCHIVED]]}}` at the beginning of the block. Any TODOs or DONEs will be replaced with an ARCHIVED. If an ARCHIVED exists, it will be replaced with a TODO. If none of the above exists, an ARCHIVED is inserted in the block.

To change the CSS styling of the archive display, you'll want to change the CSS associated with the `roamjs-todont` class.

Expand Down
157 changes: 106 additions & 51 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,100 +271,146 @@ export default runExtension(async ({ extensionAPI }) => {
return { explode: !!extensionAPI.settings.get("explode") };
};

type TodoState = "TODO" | "DONE";
const latestHandledTransition = new Map<
string,
{ state: TodoState; timestamp: number }
>();
const HANDLED_TRANSITION_WINDOW_MS = 300;
const ROAM_STATE_SETTLE_MS = 50;
const hasHandledRecently = (blockUid: string, state: TodoState) => {
const now = Date.now();
if (latestHandledTransition.size > 100) {
for (const [uid, entry] of latestHandledTransition) {
if (now - entry.timestamp > HANDLED_TRANSITION_WINDOW_MS) {
latestHandledTransition.delete(uid);
}
}
}
const previous = latestHandledTransition.get(blockUid);
latestHandledTransition.set(blockUid, { state, timestamp: now });
return (
!!previous &&
previous.state === state &&
now - previous.timestamp < HANDLED_TRANSITION_WINDOW_MS
);
};

const triggerOnTodo = (blockUid: string, value: string) => {
if (!hasHandledRecently(blockUid, "TODO")) {
onTodo(blockUid, value);
}
};

const triggerOnDone = (blockUid: string, value: string) => {
if (hasHandledRecently(blockUid, "DONE")) {
return { explode: false };
}
return onDone(blockUid, value);
};

createHTMLObserver({
tag: "LABEL",
className: "check-container",
callback: (_l) => {
const l = _l as HTMLLabelElement;
const inputTarget = l.querySelector("input");
if (inputTarget?.type === "checkbox") {
if (
inputTarget?.type === "checkbox" &&
inputTarget.dataset.todoTriggerBound !== "true"
) {
inputTarget.dataset.todoTriggerBound = "true";
const blockUid = getBlockUidFromTarget(inputTarget);
inputTarget.addEventListener("click", () => {
const position = inputTarget.getBoundingClientRect();
setTimeout(() => {
const oldValue = getTextByBlockUid(blockUid);
const value = getTextByBlockUid(blockUid);
if (inputTarget.checked) {
onTodo(blockUid, oldValue);
} else {
const config = onDone(blockUid, oldValue);
const config = triggerOnDone(blockUid, value);
if (config.explode) {
setTimeout(() => {
explode(position.x, position.y);
}, 50);
}
} else {
triggerOnTodo(blockUid, value);
}
}, 50);
}, ROAM_STATE_SETTLE_MS);
});
}
},
});

const clickListener = async (e: MouseEvent) => {
const clickListener = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.parentElement?.getElementsByClassName(
"bp3-text-overflow-ellipsis"
)[0]?.innerHTML === "TODO"
) {
const textarea = target
.closest(".roam-block-container")
?.getElementsByTagName?.("textarea")?.[0];
if (textarea) {
const { blockUid } = getUids(textarea);
onTodo(blockUid, textarea.value);
}
const menuItem = target.closest(".bp3-menu-item") as HTMLElement;
const menuLabel = menuItem
?.querySelector(".bp3-text-overflow-ellipsis")
?.textContent?.trim();
if (menuLabel === "TODO") {
setTimeout(() => {
const blockUid = window.roamAlphaAPI.ui.getFocusedBlock()?.["block-uid"];
if (!blockUid) {
return;
}
const value = getTextByBlockUid(blockUid);
if (value.startsWith("{{[[TODO]]}}")) {
triggerOnTodo(blockUid, value);
}
}, ROAM_STATE_SETTLE_MS);
}
};
document.addEventListener("click", clickListener);

const keydownEventListener = async (_e: Event) => {
const keydownEventListener = (_e: Event) => {
const e = _e as KeyboardEvent;
if (e.key === "Enter") {
if (isControl(e)) {
const target = e.target as HTMLElement;
if (target.tagName === "TEXTAREA") {
const textArea = target as HTMLTextAreaElement;
const { blockUid } = getUids(textArea);
if (textArea.value.startsWith("{{[[DONE]]}}")) {
onDone(blockUid, textArea.value);
} else if (textArea.value.startsWith("{{[[TODO]]}}")) {
onTodo(blockUid, textArea.value);
}
setTimeout(() => {
const value = getTextByBlockUid(blockUid);
if (value.startsWith("{{[[DONE]]}}")) {
triggerOnDone(blockUid, value);
} else if (value.startsWith("{{[[TODO]]}}")) {
triggerOnTodo(blockUid, value);
}
}, ROAM_STATE_SETTLE_MS);
return;
}
Array.from(document.getElementsByClassName("block-highlight-blue"))
const blockUids = Array.from(
document.getElementsByClassName("block-highlight-blue")
)
.map(
(d) => d.getElementsByClassName("roam-block")[0] as HTMLDivElement
)
.map((d) => getUids(d).blockUid)
.map((blockUid) => ({
blockUid,
text: getTextByBlockUid(blockUid),
}))
.forEach(({ blockUid, text }) => {
if (text.startsWith("{{[[DONE]]}}")) {
onTodo(blockUid, text);
} else if (text.startsWith("{{[[TODO]]}}")) {
onDone(blockUid, text);
.map((d) => getUids(d).blockUid);
setTimeout(() => {
blockUids.forEach((blockUid) => {
const value = getTextByBlockUid(blockUid);
if (!value) return;
if (value.startsWith("{{[[DONE]]}}")) {
triggerOnDone(blockUid, value);
} else if (value.startsWith("{{[[TODO]]}}")) {
triggerOnTodo(blockUid, value);
}
});
}, ROAM_STATE_SETTLE_MS);
} else {
const target = e.target as HTMLElement;
if (target.tagName === "TEXTAREA") {
const todoItem = Array.from(
target.parentElement?.querySelectorAll<HTMLDivElement>(
".bp3-text-overflow-ellipsis"
) || []
).find((t) => t.innerText === "TODO");
if (
todoItem &&
todoItem.parentElement &&
getComputedStyle(todoItem.parentElement).backgroundColor ===
"rgb(213, 218, 223)"
) {
const textArea = target as HTMLTextAreaElement;
const { blockUid } = getUids(textArea);
onTodo(blockUid, textArea.value);
const textArea = target as HTMLTextAreaElement;
const beforeValue = textArea.value;
const { blockUid } = getUids(textArea);
if (!beforeValue.startsWith("{{[[TODO]]}}")) {
setTimeout(() => {
const value = getTextByBlockUid(blockUid);
if (value.startsWith("{{[[TODO]]}}")) {
triggerOnTodo(blockUid, value);
}
}, ROAM_STATE_SETTLE_MS);
}
}
}
Expand Down Expand Up @@ -436,8 +482,17 @@ export default runExtension(async ({ extensionAPI }) => {

return {
domListeners: [
{ type: "keydown", el: document, listener: keydownEventListener },
{ type: "keydown" as const, el: document, listener: keydownEventListener },
{ type: "click" as const, el: document, listener: clickListener as EventListener },
],
commands: ["Defer TODO"],
unload: () => {
document
.querySelectorAll<HTMLInputElement>("[data-todo-trigger-bound]")
.forEach((el) => {
delete el.dataset.todoTriggerBound;
});
latestHandledTransition.clear();
},
};
});
3 changes: 1 addition & 2 deletions src/utils/todont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ const initializeTodont = () => {
} else if (firstButtonTag === "ARCHIVED") {
replaceText({
before: "{{[[ARCHIVED]]}}",
after: "",
prepend: true,
after: "{{[[TODO]]}}",
});
} else {
replaceText({
Expand Down