diff --git a/apps/roam/src/components/DiscourseContextOverlay.tsx b/apps/roam/src/components/DiscourseContextOverlay.tsx index 88aacee4..eae6bb1a 100644 --- a/apps/roam/src/components/DiscourseContextOverlay.tsx +++ b/apps/roam/src/components/DiscourseContextOverlay.tsx @@ -93,6 +93,8 @@ type DiscourseContextOverlayBaseProps = { type DiscourseContextOverlayProps = DiscourseContextOverlayBaseProps & ({ tag: string; uid?: never } | { tag?: never; uid: string }); +const ICON_SIZE = 16; + const DiscourseContextOverlay = ({ tag, id, @@ -169,6 +171,7 @@ const DiscourseContextOverlay = ({ icon={"diagram-tree"} color={iconColor} style={{ opacity: `${Number(opacity) / 100}` }} + size={ICON_SIZE} /> { ); diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index b6fb54db..e10d05ad 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -23,7 +23,8 @@ import { addPageRefObserver, getPageRefObserversSize, previewPageRefHandler, - overlayPageRefHandler, + getOverlayHandler, + onPageRefObserverChange, getSuggestiveOverlayHandler, } from "~/utils/pageRefObserverHandlers"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; @@ -186,7 +187,8 @@ export const initObservers = async ({ if (onloadArgs.extensionAPI.settings.get("page-preview")) addPageRefObserver(previewPageRefHandler); if (onloadArgs.extensionAPI.settings.get("discourse-context-overlay")) { - addPageRefObserver((s) => overlayPageRefHandler(s, onloadArgs)); + const overlayHandler = getOverlayHandler(onloadArgs); + onPageRefObserverChange(overlayHandler)(true); } if (!!getPageRefObserversSize()) enablePageRefObserver(); diff --git a/apps/roam/src/utils/pageRefObserverHandlers.ts b/apps/roam/src/utils/pageRefObserverHandlers.ts index 6acc9623..f0068980 100644 --- a/apps/roam/src/utils/pageRefObserverHandlers.ts +++ b/apps/roam/src/utils/pageRefObserverHandlers.ts @@ -32,9 +32,24 @@ export const overlayPageRefHandler = ( const tag = s.getAttribute("data-tag") || s.parentElement.getAttribute("data-link-title"); + const hasOverlayAttribute = s.getAttribute("data-roamjs-discourse-overlay"); + const hasOverlayElement = + (s.hasAttribute("data-tag") && + Array.from(s.children).some( + (child) => + child instanceof HTMLSpanElement && + child.querySelector(".roamjs-discourse-context-overlay"), + )) || + (s.parentElement && + Array.from(s.parentElement.children).some( + (child) => + child instanceof HTMLSpanElement && + child.querySelector(".roamjs-discourse-context-overlay"), + )); if ( tag && - !s.getAttribute("data-roamjs-discourse-overlay") && + !hasOverlayAttribute && + !hasOverlayElement && isDiscourseNode(getPageUidByPageTitle(tag)) ) { s.setAttribute("data-roamjs-discourse-overlay", "true"); @@ -116,13 +131,64 @@ const disablePageRefObserver = () => { pageRefObserverRef.current = undefined; }; +const applyHandlersToExistingPageRefs = ( + handler: (s: HTMLSpanElement) => void, +) => { + const existingPageRefs = + document.querySelectorAll("span.rm-page-ref"); + existingPageRefs.forEach((pageRef) => { + handler(pageRef); + }); +}; + +const removeOverlaysFromExistingPageRefs = () => { + // Find all page refs that have the overlay attribute OR have overlay elements + // This ensures we catch all cases, even if attribute is missing + const allPageRefs = + document.querySelectorAll("span.rm-page-ref"); + allPageRefs.forEach((pageRef) => { + // Check if overlay is a direct child of pageRef + const directChildContainer = Array.from(pageRef.children).find( + (child) => + child instanceof HTMLSpanElement && + child.querySelector(".roamjs-discourse-context-overlay"), + ) as HTMLSpanElement | undefined; + if (directChildContainer) { + directChildContainer.remove(); + pageRef.removeAttribute("data-roamjs-discourse-overlay"); + return; + } + + // Check if overlay is a direct child of pageRef's parent element + if (pageRef.parentElement) { + const parentDirectChildContainer = Array.from( + pageRef.parentElement.children, + ).find( + (child) => + child instanceof HTMLSpanElement && + child.querySelector(".roamjs-discourse-context-overlay"), + ) as HTMLSpanElement | undefined; + if (parentDirectChildContainer) { + parentDirectChildContainer.remove(); + pageRef.removeAttribute("data-roamjs-discourse-overlay"); + } + } + }); +}; + export const onPageRefObserverChange = (handler: (s: HTMLSpanElement) => void) => (b: boolean) => { if (b) { if (!pageRefObservers.size) enablePageRefObserver(); pageRefObservers.add(handler); + // Apply handler to existing page refs when enabling + applyHandlersToExistingPageRefs(handler); } else { pageRefObservers.delete(handler); + // Remove overlays from existing page refs when disabling + if (handler === cachedHandler) { + removeOverlaysFromExistingPageRefs(); + } if (!pageRefObservers.size) disablePageRefObserver(); } }; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index a963e0c3..28ee9923 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -12,6 +12,10 @@ import getDiscourseNodes from "./getDiscourseNodes"; import fireQuery from "./fireQuery"; import { excludeDefaultNodes } from "~/utils/getDiscourseNodes"; import { render as renderSettings } from "~/components/settings/Settings"; +import { + getOverlayHandler, + onPageRefObserverChange, +} from "./pageRefObserverHandlers"; export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { const { extensionAPI } = onloadArgs; @@ -134,6 +138,29 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { const renderSettingsPopup = () => renderSettings({ onloadArgs }); + const toggleDiscourseContextOverlay = async () => { + const currentValue = + (extensionAPI.settings.get("discourse-context-overlay") as boolean) ?? + false; + const newValue = !currentValue; + try { + await extensionAPI.settings.set("discourse-context-overlay", newValue); + } catch (error) { + const e = error as Error; + renderToast({ + id: "discourse-context-overlay-toggle-error", + content: `Failed to toggle discourse context overlay: ${e.message}`, + }); + return; + } + const overlayHandler = getOverlayHandler(onloadArgs); + onPageRefObserverChange(overlayHandler)(newValue); + renderToast({ + id: "discourse-context-overlay-toggle", + content: `Discourse context overlay ${newValue ? "enabled" : "disabled"}`, + }); + }; + const addCommand = (label: string, callback: () => void) => { return extensionAPI.ui.commandPalette.addCommand({ label, @@ -141,11 +168,15 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }); }; - // Roam organizes commands by alphabetically - addCommand("DG: Export - Current Page", exportCurrentPage); - addCommand("DG: Export - Discourse Graph", exportDiscourseGraph); - addCommand("DG: Open - Discourse Settings", renderSettingsPopup); - addCommand("DG: Open - Query Drawer", openQueryDrawerWithArgs); - addCommand("DG: Query Block - Create", createQueryBlock); - addCommand("DG: Query Block - Refresh", refreshCurrentQueryBuilder); + // Roam organizes commands alphabetically + void addCommand("DG: Export - Current page", exportCurrentPage); + void addCommand("DG: Export - Discourse graph", exportDiscourseGraph); + void addCommand("DG: Open - Discourse settings", renderSettingsPopup); + void addCommand("DG: Open - Query drawer", openQueryDrawerWithArgs); + void addCommand( + "DG: Toggle - Discourse context overlay", + toggleDiscourseContextOverlay, + ); + void addCommand("DG: Query block - Create", createQueryBlock); + void addCommand("DG: Query block - Refresh", refreshCurrentQueryBuilder); };