diff --git a/README.md b/README.md index 381f92c..a4d030f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ This extension supports the following configuration options, to be specified in - `Send To Block` - (Optiona) Set to a page name or a block reference to send the completed TODO to be a child of that node. +- `TODONT Hotkey` - (Optional) The default hotkey string for `Archive TODO` (for example `ctrl+shift+enter`). + Anytime a TODO checkbox becomes DONE, either by user click or keyboard shortbut, the "Done" action fires. Similarly, when a DONE checkbox becomes TODO, the "Todo" action fires. This extension also works on multiple blocks at once. When "Append Text" is configured, the "Done" action appends the configured text to the end of the block. The "Todo" action removes the configured text from the end of the block. @@ -46,9 +48,9 @@ When None are configured, nothing happens. ## TODONT Mode -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. +TODONT Mode allows users to archive todos, by replacing the `{{[[TODO]]}}` with a `{{[[ARCHIVED]]}}`. To enable styling, 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. +The `Archive TODO` command is always available in the command palette and Hotkeys settings. Its default hotkey comes from the `TODONT Hotkey` extension setting (default: `ctrl+shift+enter`). 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 change the CSS styling of the archive display, you'll want to change the CSS associated with the `roamjs-todont` class. diff --git a/package.json b/package.json index 70cb77c..c4451b6 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,21 @@ "name": "todo-trigger", "version": "1.1.1", "description": "Tie special actions to converting between TODOs and DONEs!", + "license": "MIT", "main": "./build/main.js", "scripts": { "prebuild:roam": "npm install", "build:roam": "samepage build", "start": "samepage dev" }, - "license": "MIT", - "tags": [ - "automations", - "todos" - ], "dependencies": { "roamjs-components": "^0.82.0" }, "samepage": { "extends": "node_modules/roamjs-components/package.json" - } + }, + "tags": [ + "automations", + "todos" + ] } diff --git a/src/index.ts b/src/index.ts index 0eacc23..419afa5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,22 +18,20 @@ import getChildrenLengthByParentUid from "roamjs-components/queries/getChildrenL import initializeTodont, { TODONT_MODES } from "./utils/todont"; export default runExtension(async ({ extensionAPI }) => { - const toggleTodont = initializeTodont(); + const toggleTodont = initializeTodont(extensionAPI); extensionAPI.settings.panel.create({ tabTitle: "TODO Trigger", settings: [ { id: "append-text", name: "Append Text", - description: - "The text to add to the end of a block, when an item flips from TODO to DONE", + description: "The text to add to the end of a block, when an item flips from TODO to DONE", action: { type: "input", placeholder: "Finished at {now}" }, }, { id: "on-todo", name: "On Todo", - description: - "The text to add to the end of a block, when a block first becomes a TODO", + description: "The text to add to the end of a block, when a block first becomes a TODO", action: { type: "input", placeholder: "#toRead" }, }, { @@ -43,6 +41,30 @@ export default runExtension(async ({ extensionAPI }) => { "The set of pairs that you would want to be replaced upon switching between todo and done", action: { type: "input", placeholder: "#toRead, #Read" }, }, + { + id: "todont-mode", + name: "TODONT Mode", + description: "Whether to incorporate styling when TODOS turn into ARCHIVED buttons.", + action: { + type: "select", + items: TODONT_MODES.slice(0), + onChange: (e) => toggleTodont(e.target.value as (typeof TODONT_MODES)[number]), + }, + }, + { + id: "todont-hotkey", + name: "TODONT Hotkey", + description: "Default hotkey for the 'Archive TODO' command (example: ctrl+shift+enter).", + action: { + type: "input", + placeholder: "ctrl+shift+enter", + onChange: () => + toggleTodont( + ((extensionAPI.settings.get("todont-mode") as (typeof TODONT_MODES)[number]) || + "off") as (typeof TODONT_MODES)[number], + ), + }, + }, { id: "strikethrough", name: "Strikethrough", @@ -52,15 +74,13 @@ export default runExtension(async ({ extensionAPI }) => { { id: "classname", name: "Classname", - description: - "Enable to add a `roamjs-done` classname to blocks with `{{[[DONE]]}}`", + description: "Enable to add a `roamjs-done` classname to blocks with `{{[[DONE]]}}`", action: { type: "switch" }, }, { id: "trim", name: "Trim Whitespace", - description: - "Trim the whitespace at the front and end when blocks become TODO or DONE", + description: "Trim the whitespace at the front and end when blocks become TODO or DONE", action: { type: "switch" }, }, { @@ -72,34 +92,16 @@ export default runExtension(async ({ extensionAPI }) => { { id: "send-to-block", name: "Send To Block", - description: - "Specify a block reference or page name to send completed TODOs", + description: "Specify a block reference or page name to send completed TODOs", action: { type: "input", placeholder: "Block reference or page name", }, }, - { - id: "todont-mode", - name: "TODONT Mode", - description: - "Whether to incorporate styling when TODOS turn into ARCHIVED buttons.", - action: { - type: "select", - items: TODONT_MODES.slice(0), - onChange: (e) => - toggleTodont(e.target.value as (typeof TODONT_MODES)[number]), - }, - }, ], }); - const CLASSNAMES_TO_CHECK = [ - "rm-block-ref", - "kanban-title", - "kanban-card", - "roam-block", - ]; + const CLASSNAMES_TO_CHECK = ["rm-block-ref", "kanban-title", "kanban-card", "roam-block"]; const onTodo = (blockUid: string, oldValue: string) => { const text = extensionAPI.settings.get("append-text") as string; @@ -115,10 +117,7 @@ export default runExtension(async ({ extensionAPI }) => { .replace(new RegExp("\\*", "g"), "\\|") .replace("/Current Time", "[0-2][0-9]:[0-5][0-9]") .replace("/Today", `\\[\\[${DAILY_NOTE_PAGE_REGEX.source}\\]\\]`) - .replace( - /{now(?::([^}]+))?}/, - `\\[\\[${DAILY_NOTE_PAGE_REGEX.source}\\]\\]` - )}`; + .replace(/{now(?::([^}]+))?}/, `\\[\\[${DAILY_NOTE_PAGE_REGEX.source}\\]\\]`)}`; value = value.replace(new RegExp(formattedText), ""); } const replaceTags = extensionAPI.settings.get("replace-tags") as string; @@ -127,23 +126,14 @@ export default runExtension(async ({ extensionAPI }) => { const formattedPairs = pairs.map((p) => p .split(",") - .map((pp) => - pp - .trim() - .replace(/^#/, "") - .replace(/^\[\[/, "") - .replace(/\]\]$/, "") - ) - .reverse() + .map((pp) => pp.trim().replace(/^#/, "").replace(/^\[\[/, "").replace(/\]\]$/, "")) + .reverse(), ); if (formattedPairs.filter((p) => p.length === 1).length < 2) { formattedPairs.forEach(([before, after]) => { if (after) { value = value - .replace( - `#${before}`, - `#${/\s/.test(after) ? `[[${after}]]` : after}` - ) + .replace(`#${before}`, `#${/\s/.test(after) ? `[[${after}]]` : after}`) .replace(new RegExp(`\\[\\[${before}\\]\\]`), `[[${after}]]`); } else { value = `${value}#[[${before}]]`; @@ -157,24 +147,16 @@ export default runExtension(async ({ extensionAPI }) => { const today = new Date(); const formattedText = ` ${onTodo .replace("/Current Time", format(today, "HH:mm")) - .replace( - "/Today", - `[[${window.roamAlphaAPI.util.dateToPageTitle(today)}]]` - ) + .replace("/Today", `[[${window.roamAlphaAPI.util.dateToPageTitle(today)}]]`) .replace(/{now(?::([^}]+))?}/, (_: string, group: string) => { const date = window.roamAlphaAPI.util.dateToPageTitle(today); - if ( - /skip dnp/i.test(group) && - date === getPageTitleByBlockUid(blockUid) - ) { + if (/skip dnp/i.test(group) && date === getPageTitleByBlockUid(blockUid)) { return ""; } else { return `[[${date}]]`; } })}`; - value = value.includes(formattedText) - ? value - : `${value}${formattedText}`; + value = value.includes(formattedText) ? value : `${value}${formattedText}`; } const trim = extensionAPI.settings.get("trim") as boolean; if (trim) { @@ -195,16 +177,10 @@ export default runExtension(async ({ extensionAPI }) => { const today = new Date(); const formattedText = ` ${text .replace("/Current Time", format(today, "HH:mm")) - .replace( - "/Today", - `[[${window.roamAlphaAPI.util.dateToPageTitle(today)}]]` - ) + .replace("/Today", `[[${window.roamAlphaAPI.util.dateToPageTitle(today)}]]`) .replace(/{now(?::([^}]+))?}/, (_: string, group: string) => { const date = window.roamAlphaAPI.util.dateToPageTitle(today); - if ( - /skip dnp/i.test(group) && - date === getPageTitleByBlockUid(blockUid) - ) { + if (/skip dnp/i.test(group) && date === getPageTitleByBlockUid(blockUid)) { return ""; } else { return `[[${date}]]`; @@ -218,28 +194,19 @@ export default runExtension(async ({ extensionAPI }) => { const formattedPairs = pairs.map((p) => p .split(",") - .map((pp) => - pp - .trim() - .replace(/^#/, "") - .replace(/^\[\[/, "") - .replace(/\]\]$/, "") - ) + .map((pp) => pp.trim().replace(/^#/, "").replace(/^\[\[/, "").replace(/\]\]$/, "")) .map((pp) => pp === "{date}" ? DAILY_NOTE_PAGE_REGEX.source : pp === "{today}" - ? window.roamAlphaAPI.util.dateToPageTitle(new Date()) - : pp - ) + ? window.roamAlphaAPI.util.dateToPageTitle(new Date()) + : pp, + ), ); formattedPairs.forEach(([before, after]) => { if (after) { value = value - .replace( - `#${before}`, - `#${/\s/.test(after) ? `[[${after}]]` : after}` - ) + .replace(`#${before}`, `#${/\s/.test(after) ? `[[${after}]]` : after}`) .replace(new RegExp(`\\[\\[${before}\\]\\]`), `[[${after}]]`); } else { value = value.replace(createTagRegex(before), ""); @@ -257,9 +224,7 @@ export default runExtension(async ({ extensionAPI }) => { } const sendToBlock = extensionAPI.settings.get("send-to-block") as string; if (sendToBlock) { - const uid = extractRef( - getPageUidByPageTitle(extractTag(sendToBlock)) || sendToBlock - ); + const uid = extractRef(getPageUidByPageTitle(extractTag(sendToBlock)) || sendToBlock); if (uid) { const bottom = getChildrenLengthByParentUid(uid); window.roamAlphaAPI.moveBlock({ @@ -302,9 +267,8 @@ export default runExtension(async ({ extensionAPI }) => { const clickListener = async (e: MouseEvent) => { const target = e.target as HTMLElement; if ( - target.parentElement?.getElementsByClassName( - "bp3-text-overflow-ellipsis" - )[0]?.innerHTML === "TODO" + target.parentElement?.getElementsByClassName("bp3-text-overflow-ellipsis")[0]?.innerHTML === + "TODO" ) { const textarea = target .closest(".roam-block-container") @@ -333,9 +297,7 @@ export default runExtension(async ({ extensionAPI }) => { return; } Array.from(document.getElementsByClassName("block-highlight-blue")) - .map( - (d) => d.getElementsByClassName("roam-block")[0] as HTMLDivElement - ) + .map((d) => d.getElementsByClassName("roam-block")[0] as HTMLDivElement) .map((d) => getUids(d).blockUid) .map((blockUid) => ({ blockUid, @@ -352,15 +314,13 @@ export default runExtension(async ({ extensionAPI }) => { const target = e.target as HTMLElement; if (target.tagName === "TEXTAREA") { const todoItem = Array.from( - target.parentElement?.querySelectorAll( - ".bp3-text-overflow-ellipsis" - ) || [] + target.parentElement?.querySelectorAll(".bp3-text-overflow-ellipsis") || + [], ).find((t) => t.innerText === "TODO"); if ( todoItem && todoItem.parentElement && - getComputedStyle(todoItem.parentElement).backgroundColor === - "rgb(213, 218, 223)" + getComputedStyle(todoItem.parentElement).backgroundColor === "rgb(213, 218, 223)" ) { const textArea = target as HTMLTextAreaElement; const { blockUid } = getUids(textArea); @@ -395,28 +355,24 @@ export default runExtension(async ({ extensionAPI }) => { if (input.checked && !input.disabled) { const zoom = l.closest(".rm-zoom-item-content") as HTMLSpanElement; if (zoom) { - styleBlock( - zoom.firstElementChild?.firstElementChild as HTMLDivElement - ); + styleBlock(zoom.firstElementChild?.firstElementChild as HTMLDivElement); return; } - const block = CLASSNAMES_TO_CHECK.map( - (c) => l.closest(`.${c}`) as HTMLElement - ).find((d) => !!d); + const block = CLASSNAMES_TO_CHECK.map((c) => l.closest(`.${c}`) as HTMLElement).find( + (d) => !!d, + ); if (block) { styleBlock(block); } } else { const zoom = l.closest(".rm-zoom-item-content") as HTMLSpanElement; if (zoom) { - unstyleBlock( - zoom.firstElementChild?.firstElementChild as HTMLDivElement - ); + unstyleBlock(zoom.firstElementChild?.firstElementChild as HTMLDivElement); return; } - const block = CLASSNAMES_TO_CHECK.map( - (c) => l.closest(`.${c}`) as HTMLElement - ).find((d) => !!d); + const block = CLASSNAMES_TO_CHECK.map((c) => l.closest(`.${c}`) as HTMLElement).find( + (d) => !!d, + ); if (block) { unstyleBlock(block); } @@ -429,15 +385,11 @@ export default runExtension(async ({ extensionAPI }) => { addDeferTODOsCommand(); toggleTodont( - (extensionAPI.settings.get( - "todont-mode" - ) as (typeof TODONT_MODES)[number]) || "off" + (extensionAPI.settings.get("todont-mode") as (typeof TODONT_MODES)[number]) || "off", ); return { - domListeners: [ - { type: "keydown", el: document, listener: keydownEventListener }, - ], + domListeners: [{ type: "keydown", el: document, listener: keydownEventListener }], commands: ["Defer TODO"], }; }); diff --git a/src/utils/deferTodos.tsx b/src/utils/deferTodos.tsx index 0bedfbc..4a00d00 100644 --- a/src/utils/deferTodos.tsx +++ b/src/utils/deferTodos.tsx @@ -24,7 +24,7 @@ const Prompt = ({ onClose, resolve }: { onClose: () => void } & Props) => { onClose(); setTimeout(() => resolve(s), 1); }, - [resolve, onClose] + [resolve, onClose], ); const contentRef = useRef(null); useEffect(() => { @@ -34,8 +34,7 @@ const Prompt = ({ onClose, resolve }: { onClose: () => void } & Props) => { }, [loaded, setLoaded]); useEffect(() => { if (contentRef.current && loaded) { - contentRef.current.closest(".bp3-overlay").style.zIndex = - "1000"; + contentRef.current.closest(".bp3-overlay").style.zIndex = "1000"; } }, [contentRef, loaded]); return ( @@ -82,9 +81,7 @@ const deferTodos = (blockUid: string) => { if (!foundButton) { //No button found which means no date variable set so need to parse out a DNP date //Find the last date in the block - var arrDnp = blockText.split( - /(\[\[[^\]]*?\s[0-9]+(?:st|nd|rd|th),\s[0-9]{4}\]\])/ - ); + var arrDnp = blockText.split(/(\[\[[^\]]*?\s[0-9]+(?:st|nd|rd|th),\s[0-9]{4}\]\])/); if (arrDnp.length > 1) { var lastDate = arrDnp[arrDnp.length - 2]; } else { @@ -105,7 +102,7 @@ const deferTodos = (blockUid: string) => { var finalString = ""; var rmDateParse = new Date(Date.parse(rmDateFormatStr)); new Promise((resolve) => - createOverlayRender("defer-todos-prompt", Prompt)({ resolve }) + createOverlayRender("defer-todos-prompt", Prompt)({ resolve }), ).then((howManyDays) => { var howManyDaysInt = parseInt(howManyDays); @@ -118,13 +115,11 @@ const deferTodos = (blockUid: string) => { if (curDayOfWeek == 6) { curDayOfWeek = 0; } - howManyDaysInt = - 1 + Math.floor(Math.random() * Math.floor(6 - curDayOfWeek)); + howManyDaysInt = 1 + Math.floor(Math.random() * Math.floor(6 - curDayOfWeek)); break; case "nw": var curDayOfWeek = rmDateParse.getDay(); - howManyDaysInt = - 6 - curDayOfWeek + 1 + Math.floor(Math.random() * Math.floor(7)); + howManyDaysInt = 6 - curDayOfWeek + 1 + Math.floor(Math.random() * Math.floor(7)); break; case "tm": var curDayOfMonth = rmDateParse.getDate(); @@ -135,10 +130,7 @@ const deferTodos = (blockUid: string) => { endOfMonthDate = endOfNextMonth; } howManyDaysInt = - 1 + - Math.floor( - Math.random() * Math.floor(endOfMonthDate - curDayOfMonth) - ); + 1 + Math.floor(Math.random() * Math.floor(endOfMonthDate - curDayOfMonth)); break; case "nm": var curDayOfMonth = rmDateParse.getDate(); @@ -151,20 +143,12 @@ const deferTodos = (blockUid: string) => { Math.floor(Math.random() * Math.floor(endOfNextMonth)); break; case "ty": - var daysLeftInYear = differenceInDays( - endOfYear(rmDateParse), - new Date() - ); - howManyDaysInt = - 1 + Math.floor(Math.random() * Math.floor(daysLeftInYear)); + var daysLeftInYear = differenceInDays(endOfYear(rmDateParse), new Date()); + howManyDaysInt = 1 + Math.floor(Math.random() * Math.floor(daysLeftInYear)); break; case "ny": - var daysLeftInYear = differenceInDays( - endOfYear(rmDateParse), - new Date() - ); - howManyDaysInt = - 1 + daysLeftInYear + Math.floor(Math.random() * Math.floor(365)); + var daysLeftInYear = differenceInDays(endOfYear(rmDateParse), new Date()); + howManyDaysInt = 1 + daysLeftInYear + Math.floor(Math.random() * Math.floor(365)); break; default: howManyDaysInt = 1; @@ -175,7 +159,7 @@ const deferTodos = (blockUid: string) => { var nextDate = new Date( rmDateParse.getFullYear(), rmDateParse.getMonth(), - rmDateParse.getDate() + howManyDaysInt + rmDateParse.getDate() + howManyDaysInt, ); var rmDateFormat = "[[" + dateFnsFormat(nextDate, "MMMM do, yyyy") + "]]"; rmDateFormatStr = rmDateFormat @@ -188,7 +172,7 @@ const deferTodos = (blockUid: string) => { .split(/(\(Deferrals:.*\))/)[1] .replace("(Deferrals:", "") .replace(")", "") - .trim() + .trim(), ); if (Number.isInteger(foundNum)) { btnCounter = foundNum; @@ -220,8 +204,8 @@ const deferTodos = (blockUid: string) => { updateBlock({ uid: blockUid, text: `${blockText.replace( - /\[\[[a-zA-Z]+ \d{1\,2}[sthndr]{2}\, \d{4}\]\] \(Deferrals:.*$/, - "" + /\[\[[a-zA-Z]+ \d{1,2}[sthndr]{2}, \d{4}\]\] \(Deferrals:.*$/, + "", )} ${finalString}`, }); }); diff --git a/src/utils/exploder.ts b/src/utils/exploder.ts index 0ba6c16..517103f 100644 --- a/src/utils/exploder.ts +++ b/src/utils/exploder.ts @@ -1,63 +1,30 @@ -const requestedFrames: number[] = []; -const explode = (x: number, y: number) => { - const colors = ["#ffc000", "#ff3b3b", "#ff8400"]; - const bubbles = 25; - let particles = []; - let ratio = window.devicePixelRatio; - let c = document.createElement("canvas"); - let ctx = c.getContext("2d"); - - c.style.position = "absolute"; - c.style.left = x - 100 + "px"; - c.style.top = y - 100 + "px"; - c.style.pointerEvents = "none"; - c.style.width = 200 + "px"; - c.style.height = 200 + "px"; - c.style.zIndex = "10000"; - c.width = 200 * ratio; - c.height = 200 * ratio; - document.body.appendChild(c); - - for (var i = 0; i < bubbles; i++) { - particles.push({ - x: c.width / 2, - y: c.height / 2, - radius: r(20, 30), - color: colors[Math.floor(Math.random() * colors.length)], - rotation: r(0, 360, true), - speed: r(8, 12), - friction: 0.9, - opacity: r(0, 0.5, true), - yVel: 0, - gravity: 0.1, - }); - } - - render(particles, ctx, c.width, c.height); - setTimeout(() => { - document.body.removeChild(c); - requestedFrames.forEach((frame) => cancelAnimationFrame(frame)); - }, 1000); +type Particle = { + x: number; + y: number; + speed: number; + rotation: number; + opacity: number; + radius: number; + friction: number; + yVel: number; + gravity: number; + color: string; }; -const render = (particles: { - x: number; - y: number; - speed: number; - rotation: number; - opacity: number; - radius: number; - friction: number; - yVel: number; - gravity: number; - color: string; -}[], ctx: CanvasRenderingContext2D, width: number, height: number) => { - requestedFrames.push( - requestAnimationFrame(() => render(particles, ctx, width, height)) - ); +const render = ({ + particles, + ctx, + width, + height, +}: { + particles: Particle[]; + ctx: CanvasRenderingContext2D; + width: number; + height: number; +}) => { ctx.clearRect(0, 0, width, height); - particles.forEach((p, i) => { + particles.forEach((p) => { p.x += p.speed * Math.cos((p.rotation * Math.PI) / 180); p.y += p.speed * Math.sin((p.rotation * Math.PI) / 180); @@ -67,7 +34,9 @@ const render = (particles: { p.yVel += p.gravity; p.y += p.yVel; - if (p.opacity < 0 || p.radius < 0) return; + if (p.opacity < 0 || p.radius < 0) { + return; + } ctx.beginPath(); ctx.globalAlpha = p.opacity; @@ -75,15 +44,64 @@ const render = (particles: { ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, false); ctx.fill(); }); - - return ctx; }; -const r = (a: number, b: number, c?: boolean) => - parseFloat( - (Math.random() * ((a ? a : 1) - (b ? b : 0)) + (b ? b : 0)).toFixed( - c ? 3 : 0 - ) - ); +const randomBetween = (min: number, max: number, withDecimals?: boolean) => + parseFloat((Math.random() * (max - min) + min).toFixed(withDecimals ? 3 : 0)); + +const explode = (x: number, y: number) => { + const colors = ["#ffc000", "#ff3b3b", "#ff8400"]; + const bubbles = 25; + const particles: Particle[] = []; + const ratio = window.devicePixelRatio || 1; + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) { + return; + } + + canvas.style.position = "absolute"; + canvas.style.left = `${x - 100}px`; + canvas.style.top = `${y - 100}px`; + canvas.style.pointerEvents = "none"; + canvas.style.width = "200px"; + canvas.style.height = "200px"; + canvas.style.zIndex = "10000"; + canvas.width = 200 * ratio; + canvas.height = 200 * ratio; + document.body.appendChild(canvas); + + for (let i = 0; i < bubbles; i++) { + particles.push({ + x: canvas.width / 2, + y: canvas.height / 2, + radius: randomBetween(20, 30), + color: colors[Math.floor(Math.random() * colors.length)], + rotation: randomBetween(0, 360, true), + speed: randomBetween(8, 12), + friction: 0.9, + opacity: randomBetween(0, 0.5, true), + yVel: 0, + gravity: 0.1, + }); + } + + let rafId = 0; + let isActive = true; + const draw = () => { + if (!isActive) { + return; + } + render({ particles, ctx, width: canvas.width, height: canvas.height }); + rafId = requestAnimationFrame(draw); + }; + + draw(); + setTimeout(() => { + isActive = false; + cancelAnimationFrame(rafId); + canvas.remove(); + }, 1000); +}; export default explode; diff --git a/src/utils/todont.ts b/src/utils/todont.ts index b6f966e..3304ea2 100644 --- a/src/utils/todont.ts +++ b/src/utils/todont.ts @@ -1,27 +1,13 @@ -import { createConfigObserver } from "roamjs-components/components/ConfigPage"; -import getSubTree from "roamjs-components/util/getSubTree"; -import runExtension from "roamjs-components/util/runExtension"; -import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; -import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import createHTMLObserver from "roamjs-components/dom/createHTMLObserver"; import createObserver from "roamjs-components/dom/createObserver"; -import toFlexRegex from "roamjs-components/util/toFlexRegex"; -import isControl from "roamjs-components/util/isControl"; -import getUids from "roamjs-components/dom/getUids"; import { OnloadArgs } from "roamjs-components/types"; -const CLASSNAMES_TO_CHECK = [ - "rm-block-ref", - "kanban-title", - "kanban-card", - "roam-block", -]; +const CLASSNAMES_TO_CHECK = ["rm-block-ref", "kanban-title", "kanban-card", "roam-block"]; const createMobileIcon = (id: string, iconType: string): HTMLButtonElement => { const iconButton = document.createElement("button"); iconButton.id = id; - iconButton.className = - "bp3-button bp3-minimal rm-mobile-button dont-unfocus-block"; + iconButton.className = "bp3-button bp3-minimal rm-mobile-button dont-unfocus-block"; iconButton.style.padding = "6px 4px 4px;"; const icon = document.createElement("i"); icon.className = `zmdi zmdi-hc-fw-rc zmdi-${iconType}`; @@ -60,10 +46,7 @@ export const replaceText = ({ const diff = text.length - oldValue.length; if (diff !== 0) { let index = 0; - const maxIndex = Math.min( - Math.max(oldValue.length, text.length), - Math.max(start, end) + 1 - ); + const maxIndex = Math.min(Math.max(oldValue.length, text.length), Math.max(start, end) + 1); for (index = 0; index < maxIndex; index++) { if (oldValue.charAt(index) !== text.charAt(index)) { break; @@ -82,13 +65,59 @@ export const replaceText = ({ export const TODONT_MODES = ["off", "icon", "strikethrough"] as const; -const initializeTodont = () => { +const ARCHIVE_COMMAND_LABEL = "Archive TODO"; + +const initializeTodont = (extensionAPI: OnloadArgs["extensionAPI"]) => { const unloads = new Set<() => void>(); - return async (todontMode: typeof TODONT_MODES[number]) => { - if (todontMode !== "off") { - const TODONT_CLASSNAME = "roamjs-todont"; - const css = document.createElement("style"); - css.textContent = `.bp3-button.bp3-small.${TODONT_CLASSNAME} { + const todontCallback = () => { + if (document.activeElement.tagName === "TEXTAREA") { + const textArea = document.activeElement as HTMLTextAreaElement; + const firstButtonTag = /{{\[\[([A-Z]{4,8})\]\]}}/.exec(textArea.value)?.[1]; + if (firstButtonTag === "TODO") { + replaceText({ before: "{{[[TODO]]}}", after: "{{[[ARCHIVED]]}}" }); + } else if (firstButtonTag === "DONE") { + replaceText({ before: "{{[[DONE]]}}", after: "{{[[ARCHIVED]]}}" }); + } else if (firstButtonTag === "ARCHIVED") { + replaceText({ + before: "{{[[ARCHIVED]]}}", + after: "", + prepend: true, + }); + } else { + replaceText({ + before: "", + prepend: true, + after: "{{[[ARCHIVED]]}}", + }); + } + } + }; + + return (todontMode: (typeof TODONT_MODES)[number]) => { + // Clean up previous mode before setting up new one + unloads.forEach((u) => u()); + unloads.clear(); + + const configuredHotkey = ((extensionAPI.settings.get("todont-hotkey") as string) || "").trim(); + extensionAPI.ui.commandPalette.addCommand({ + label: ARCHIVE_COMMAND_LABEL, + callback: todontCallback, + "default-hotkey": configuredHotkey || "ctrl+shift+enter", + "disable-hotkey": false, + }); + unloads.add(() => { + extensionAPI.ui.commandPalette.removeCommand({ + label: ARCHIVE_COMMAND_LABEL, + }); + }); + + if (todontMode === "off") { + return; + } + + const TODONT_CLASSNAME = "roamjs-todont"; + const css = document.createElement("style"); + css.textContent = `.bp3-button.bp3-small.${TODONT_CLASSNAME} { padding: 0; min-height: 0; min-width: 0; @@ -102,142 +131,86 @@ const initializeTodont = () => { border: #EA666656; border-width: 0 2px 2px 0; }`; - document.getElementsByTagName("head")[0].appendChild(css); - unloads.add(() => { - css.remove(); - }); - - const styleArchivedButtons = (node: HTMLElement) => { - const buttons = node.getElementsByTagName("button"); - Array.from(buttons).forEach((button) => { - if ( - button.innerText === "ARCHIVED" && - button.className.indexOf(TODONT_CLASSNAME) < 0 - ) { - button.innerText = "x"; - button.className = `${button.className} ${TODONT_CLASSNAME}`; - } - }); - }; - styleArchivedButtons(document.body); - unloads.add(() => { - document - .querySelectorAll(`.${TODONT_CLASSNAME}`) - .forEach((b) => { - if (b.innerText === "x") { - b.innerText = "ARCHIVED"; - } - b.classList.remove(TODONT_CLASSNAME); - }); - }); + document.getElementsByTagName("head")[0].appendChild(css); + unloads.add(() => { + css.remove(); + }); - let previousActiveElement: HTMLElement; - const todontIconButton = createMobileIcon( - "mobile-todont-icon-button", - "minus-square" - ); - todontIconButton.onclick = () => { - if (previousActiveElement.tagName === "TEXTAREA") { - previousActiveElement.focus(); - todontCallback(); + const styleArchivedButtons = (node: HTMLElement) => { + const buttons = node.getElementsByTagName("button"); + Array.from(buttons).forEach((button) => { + if (button.innerText === "ARCHIVED" && button.className.indexOf(TODONT_CLASSNAME) < 0) { + button.innerText = "x"; + button.className = `${button.className} ${TODONT_CLASSNAME}`; } - }; - - todontIconButton.onmousedown = () => { - previousActiveElement = document.activeElement as HTMLElement; - }; - unloads.add(() => { - todontIconButton.remove(); }); - - const iconObserver = createObserver((mutationList: MutationRecord[]) => { - mutationList.forEach((record) => { - styleArchivedButtons(record.target as HTMLElement); - }); - const mobileBackButton = document.getElementById( - "mobile-back-icon-button" - ); - if ( - !!mobileBackButton && - !document.getElementById("mobile-todont-icon-button") - ) { - const mobileBar = document.getElementById("rm-mobile-bar"); - if (mobileBar) { - mobileBar.insertBefore(todontIconButton, mobileBackButton); - } + }; + styleArchivedButtons(document.body); + unloads.add(() => { + document.querySelectorAll(`.${TODONT_CLASSNAME}`).forEach((b) => { + if (b.innerText === "x") { + b.innerText = "ARCHIVED"; } + b.classList.remove(TODONT_CLASSNAME); }); - unloads.add(() => { - iconObserver.disconnect(); - }); + }); - const todontCallback = () => { - if (document.activeElement.tagName === "TEXTAREA") { - const textArea = document.activeElement as HTMLTextAreaElement; - const firstButtonTag = /{{\[\[([A-Z]{4,8})\]\]}}/.exec( - textArea.value - )?.[1]; - if (firstButtonTag === "TODO") { - replaceText({ before: "{{[[TODO]]}}", after: "{{[[ARCHIVED]]}}" }); - } else if (firstButtonTag === "DONE") { - replaceText({ before: "{{[[DONE]]}}", after: "{{[[ARCHIVED]]}}" }); - } else if (firstButtonTag === "ARCHIVED") { - replaceText({ - before: "{{[[ARCHIVED]]}}", - after: "", - prepend: true, - }); - } else { - replaceText({ - before: "", - prepend: true, - after: "{{[[ARCHIVED]]}}", - }); - } - } - }; + let previousActiveElement: HTMLElement; + const todontIconButton = createMobileIcon("mobile-todont-icon-button", "minus-square"); + todontIconButton.onclick = () => { + if (previousActiveElement.tagName === "TEXTAREA") { + previousActiveElement.focus(); + todontCallback(); + } + }; - const keydownEventListener = async (e: KeyboardEvent) => { - if (e.key === "Enter" && e.shiftKey && isControl(e)) { - todontCallback(); - } - }; + todontIconButton.onmousedown = () => { + previousActiveElement = document.activeElement as HTMLElement; + }; + unloads.add(() => { + todontIconButton.remove(); + }); - document.addEventListener("keydown", keydownEventListener); - unloads.add(() => { - document.removeEventListener("keydown", keydownEventListener); + const iconObserver = createObserver((mutationList: MutationRecord[]) => { + mutationList.forEach((record) => { + styleArchivedButtons(record.target as HTMLElement); }); + const mobileBackButton = document.getElementById("mobile-back-icon-button"); + if (!!mobileBackButton && !document.getElementById("mobile-todont-icon-button")) { + const mobileBar = document.getElementById("rm-mobile-bar"); + if (mobileBar) { + mobileBar.insertBefore(todontIconButton, mobileBackButton); + } + } + }); + unloads.add(() => { + iconObserver.disconnect(); + }); - if (todontMode === "strikethrough") { - const styleBlock = (block?: HTMLElement) => { + if (todontMode === "strikethrough") { + const styleBlock = (block?: HTMLElement) => { + if (block) { + block.style.textDecoration = "line-through"; + } + }; + const strikethroughObserver = createHTMLObserver({ + callback: (b: HTMLButtonElement) => { + const zoom = b.closest(".rm-zoom-item-content") as HTMLSpanElement; + if (zoom) { + styleBlock(zoom.firstElementChild.firstElementChild as HTMLDivElement); + return; + } + const block = CLASSNAMES_TO_CHECK.map((c) => b.closest(`.${c}`) as HTMLElement).find( + (d) => !!d, + ); if (block) { - block.style.textDecoration = "line-through"; + styleBlock(block); } - }; - const strikethroughObserver = createHTMLObserver({ - callback: (b: HTMLButtonElement) => { - const zoom = b.closest(".rm-zoom-item-content") as HTMLSpanElement; - if (zoom) { - styleBlock( - zoom.firstElementChild.firstElementChild as HTMLDivElement - ); - return; - } - const block = CLASSNAMES_TO_CHECK.map( - (c) => b.closest(`.${c}`) as HTMLElement - ).find((d) => !!d); - if (block) { - styleBlock(block); - } - }, - tag: "BUTTON", - className: TODONT_CLASSNAME, - }); - unloads.add(() => strikethroughObserver.disconnect()); - } - } else { - unloads.forEach((u) => u()); - unloads.clear(); + }, + tag: "BUTTON", + className: TODONT_CLASSNAME, + }); + unloads.add(() => strikethroughObserver.disconnect()); } }; };