From d9eea0843b1e604ef7c193ddd82d2140c91bc606 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 18 May 2026 20:04:36 -0600 Subject: [PATCH 1/3] Add table handling and event management in attribute selection component * Introduced functions to handle table rows and retrieve block UIDs from table cells. * Enhanced event management for attribute buttons to prevent default actions and improve user interaction. * Refactored attribute button rendering to include new event handlers for better focus and click behavior. * Updated various function calls to maintain consistency and prevent potential errors. * Improved code readability and maintainability with structured event handling. --- src/features/attributeSelect.tsx | 139 +++++++++++++++++++++++++------ 1 file changed, 115 insertions(+), 24 deletions(-) diff --git a/src/features/attributeSelect.tsx b/src/features/attributeSelect.tsx index b1c927b3..ec13cd48 100644 --- a/src/features/attributeSelect.tsx +++ b/src/features/attributeSelect.tsx @@ -22,7 +22,11 @@ import getBlockUidFromTarget from "roamjs-components/dom/getBlockUidFromTarget"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import createBlock from "roamjs-components/writes/createBlock"; -import { InputTextNode, PullBlock } from "roamjs-components/types"; +import { + InputTextNode, + PullBlock, + RoamBasicNode, +} from "roamjs-components/types"; import getSubTree from "roamjs-components/util/getSubTree"; import updateBlock from "roamjs-components/writes/updateBlock"; import deleteBlock from "roamjs-components/writes/deleteBlock"; @@ -86,6 +90,67 @@ type FormatParams = { customReplacement?: string; }; +/* Handle roam {[[table]]} */ +const getTableRows = (nodes: RoamBasicNode[]): RoamBasicNode[][] => { + const rows: RoamBasicNode[][] = []; + const visit = (node: RoamBasicNode, path: RoamBasicNode[]) => { + const nextPath = [...path, node]; + if (!node.children.length) { + rows.push(nextPath); + return; + } + node.children.forEach((child) => visit(child, nextPath)); + }; + + nodes.forEach((node) => visit(node, [])); + return rows; +}; + +const getTableCellBlockUidFromTarget = ( + target: HTMLElement, +): string | undefined => { + const cell = target.closest( + "td[data-row][data-col]", + ) as HTMLTableCellElement | null; + const table = cell?.closest(".rm-table") as HTMLElement | null; + if (!cell || !table) return undefined; + + const row = Number(cell.getAttribute("data-row")); + const col = Number(cell.getAttribute("data-col")); + const tableBlock = table.closest(".roam-block") as HTMLElement | null; + const tableUid = tableBlock ? getBlockUidFromTarget(tableBlock) : ""; + if (!tableUid || Number.isNaN(row) || Number.isNaN(col)) return ""; + + const rows = getTableRows(getBasicTreeByParentUid(tableUid)); + return rows[row]?.[col]?.uid || ""; +}; + +const getAttributeBlockUidFromTarget = (target: HTMLElement): string => { + if (target.closest(".rm-block-ref")) return getBlockUidFromTarget(target); + + const tableCellUid = getTableCellBlockUidFromTarget(target); + if (tableCellUid !== undefined) return tableCellUid; + + return getBlockUidFromTarget(target); +}; + +const stopAttributeButtonDomEvent = (e: Event) => { + if (e.type === "mousedown") e.preventDefault(); + e.stopPropagation(); +}; + +const stopAttributeButtonReactEvent = ( + e: React.SyntheticEvent, +) => { + e.stopPropagation(); +}; + +const preventAttributeButtonFocus = (e: React.SyntheticEvent) => { + e.preventDefault(); + e.stopPropagation(); +}; +/* Handle roam {[[table]]} */ + const applyFormatting = ({ text, templateName, @@ -178,7 +243,7 @@ const AttributeButtonPopover = ({ text, ...formatConfig, }), - [formatConfig] + [formatConfig], ); // Only show filter if we have more than 10 items @@ -221,7 +286,15 @@ const AttributeButtonPopover = ({ style={{ minHeight: 15, minWidth: 20 }} intent="primary" minimal - onClick={() => setIsOpen(true)} + tabIndex={-1} + onPointerDown={stopAttributeButtonReactEvent} + onMouseDown={preventAttributeButtonFocus} + onMouseUp={stopAttributeButtonReactEvent} + onDoubleClick={stopAttributeButtonReactEvent} + onClick={(e) => { + stopAttributeButtonReactEvent(e); + setIsOpen(true); + }} /> )} @@ -331,7 +404,15 @@ const AttributeButton = ({ style={{ minHeight: 15, minWidth: 20 }} intent="primary" minimal - onClick={() => setIsOpen(true)} + tabIndex={-1} + onPointerDown={stopAttributeButtonReactEvent} + onMouseDown={preventAttributeButtonFocus} + onMouseUp={stopAttributeButtonReactEvent} + onDoubleClick={stopAttributeButtonReactEvent} + onClick={(e) => { + stopAttributeButtonReactEvent(e); + setIsOpen(true); + }} /> } onClose={() => setIsOpen(false)} @@ -354,13 +435,22 @@ const AttributeButton = ({ const renderAttributeButton = ( parent: HTMLSpanElement, attributeName: string, - blockUid: string + blockUid: string, ) => { const containerSpan = document.createElement("span"); - containerSpan.onmousedown = (e) => e.stopPropagation(); + [ + "pointerdown", + "mousedown", + "mouseup", + "click", + "dblclick", + "focusin", + ].forEach((eventName) => + containerSpan.addEventListener(eventName, stopAttributeButtonDomEvent), + ); ReactDOM.render( , - containerSpan + containerSpan, ); parent.appendChild(containerSpan); }; @@ -376,7 +466,7 @@ const AttributeConfigPanel = ({ const [query, setQuery] = useState(""); const [value, setValue] = useState(""); const [definedAttributes, setDefinedAttributes] = useState(() => - getDefinedAttributes() + getDefinedAttributes(), ); const [activeTab, setActiveTab] = useState(definedAttributes[0]); const handleTabChange = (tabName: string) => { @@ -418,7 +508,7 @@ const AttributeConfigPanel = ({ [(get ?d :value) ?s] [(untuple ?s) [?e ?uid]] [?page :block/uid ?uid] - ]` + ]`, )) as [PullBlock][]; const attributesInGraph = results.map((p) => p[0]?.[":node/title"] || ""); if (attributesInGraph.length === 0) { @@ -430,17 +520,17 @@ const AttributeConfigPanel = ({ }; const focusAndOpenSelect = () => { const selectElement = document.querySelector( - ".attribute-select-autocomplete-select button" + ".attribute-select-autocomplete-select button", ) as HTMLElement; if (selectElement) selectElement.click(); }; const focusOnBlock = (uid: string) => { const el = document.querySelector( - `.attribute-${uid} .rm-api-render--block .rm-level-1 .rm-block__input` + `.attribute-${uid} .rm-api-render--block .rm-level-1 .rm-block__input`, ); if (!el) return; const match = el.id.match( - /block-input-(uuid[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12})-([\w\d]+)/i + /block-input-(uuid[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12})-([\w\d]+)/i, ); if (match) { const location = match[1]; @@ -493,7 +583,7 @@ const AttributeConfigPanel = ({ !definedAttributes.includes(a) + (a) => !definedAttributes.includes(a), )} onItemSelect={(s) => setValue(s)} activeItem={value} @@ -588,7 +678,7 @@ const TabsPanel = ({ key: "type", parentUid: attributeUid, }), - [attributeUid] + [attributeUid], ); const [optionType, setOptionType] = useState(initialOptionType || "text"); const [min, setMin] = useState(Number(rangeNode.children[0]?.text) || 0); @@ -624,7 +714,7 @@ const TabsPanel = ({ const [selectedTemplate, setSelectedTemplate] = useState(initialTemplate); const [customPattern, setCustomPattern] = useState(initialCustomPattern); const [customReplacement, setCustomReplacement] = useState( - initialCustomReplacement + initialCustomReplacement, ); const [isValidRegex, setIsValidRegex] = useState(true); @@ -648,13 +738,13 @@ const TabsPanel = ({ window.roamAlphaAPI.data.fast .q( ` - [:find ?b :where [?r :node/title "${attributeName}"] [?c :block/refs ?r] [?c :block/string ?b]]` + [:find ?b :where [?r :node/title "${attributeName}"] [?c :block/refs ?r] [?c :block/string ?b]]`, ) .map((p) => { const rawString = p[0] as string; return rawString.replace(regex, "").trim(); - }) - ) + }), + ), ) .filter((option) => option !== "") .filter((option) => !chosenOptions.includes(option)) @@ -763,7 +853,7 @@ const TabsPanel = ({ {name}:{" "} {description} - ) + ), )} @@ -910,8 +1000,8 @@ const TabsPanel = ({ } setPotentialOptions( potentialOptions.filter( - (option) => option !== selectedOption - ) + (option) => option !== selectedOption, + ), ); setSelectedOption(""); }} @@ -941,7 +1031,7 @@ const TabsPanel = ({ const getDefinedAttributes = (): string[] => { const attributesUid = window.roamAlphaAPI.data.fast.q( - `[:find ?u :where [?b :block/page ?p] [?b :block/uid ?u] [?b :block/string "attributes"] [?p :node/title "roam/js/attribute-select"]]` + `[:find ?u :where [?b :block/page ?p] [?b :block/uid ?u] [?b :block/string "attributes"] [?p :node/title "roam/js/attribute-select"]]`, )[0]?.[0] as string; const attributesTree = getBasicTreeByParentUid(attributesUid); const definedAttributes = attributesTree.map((t) => t.text); @@ -991,7 +1081,7 @@ const renderConfigPage = ({ parent.id = `${configPageId}-config`; containerParent.insertBefore( parent, - h.parentElement?.nextElementSibling || null + h.parentElement?.nextElementSibling || null, ); ReactDOM.render(, parent); } @@ -1007,12 +1097,13 @@ const updateAttributeObserver = () => { className: "rm-attr-ref", tag: "SPAN", callback: (s: HTMLSpanElement) => { - const blockUid = getBlockUidFromTarget(s); + const blockUid = getAttributeBlockUidFromTarget(s); const attributeUid = s.getAttribute("data-link-uid"); const attributeName = attributeUid ? getPageTitleByPageUid(attributeUid) : ""; if ( + blockUid && !s.hasAttribute("data-roamjs-attribute-select") && definedAttributes.includes(attributeName) ) { From 4d75513ae68d1c0553fe95faebac136b09dcb7c3 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 18 May 2026 20:06:53 -0600 Subject: [PATCH 2/3] 1.7.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cb2c395..6b696226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "workbench", - "version": "1.7.3", + "version": "1.7.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "workbench", - "version": "1.7.3", + "version": "1.7.4", "dependencies": { "@mozilla/readability": "^0.3.0", "buffer": "^6.0.3", diff --git a/package.json b/package.json index 904e5265..2fbf6358 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@types/mozilla-readability": "^0.2.0", "@types/turndown": "^5.0.1" }, - "version": "1.7.3", + "version": "1.7.4", "samepage": { "extends": "node_modules/roamjs-components/package.json" } From b831ccad17517a5a78f633fcfe8d46937b7b8ba7 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 18 May 2026 20:15:14 -0600 Subject: [PATCH 3/3] Handle missing table cell uid in attribute select --- src/features/attributeSelect.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/attributeSelect.tsx b/src/features/attributeSelect.tsx index ec13cd48..9c951105 100644 --- a/src/features/attributeSelect.tsx +++ b/src/features/attributeSelect.tsx @@ -118,18 +118,18 @@ const getTableCellBlockUidFromTarget = ( const row = Number(cell.getAttribute("data-row")); const col = Number(cell.getAttribute("data-col")); const tableBlock = table.closest(".roam-block") as HTMLElement | null; - const tableUid = tableBlock ? getBlockUidFromTarget(tableBlock) : ""; - if (!tableUid || Number.isNaN(row) || Number.isNaN(col)) return ""; + const tableUid = tableBlock ? getBlockUidFromTarget(tableBlock) : undefined; + if (!tableUid || Number.isNaN(row) || Number.isNaN(col)) return undefined; const rows = getTableRows(getBasicTreeByParentUid(tableUid)); - return rows[row]?.[col]?.uid || ""; + return rows[row]?.[col]?.uid; }; const getAttributeBlockUidFromTarget = (target: HTMLElement): string => { if (target.closest(".rm-block-ref")) return getBlockUidFromTarget(target); const tableCellUid = getTableCellBlockUidFromTarget(target); - if (tableCellUid !== undefined) return tableCellUid; + if (tableCellUid) return tableCellUid; return getBlockUidFromTarget(target); };