From 32ddc165ff0722da57247b310bf7e4c8f4b7e2c2 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Wed, 1 Oct 2025 16:25:36 -0600 Subject: [PATCH 1/3] Enhance Daily Notes Popup with date banners for headings. - Fix bug introduced by Roam DOM changes - Introduced utility functions for parsing dates and creating banners, and implemented an observer for dynamic heading updates. - Refactored initialization logic to improve readability and maintainability. --- src/features/dailyNotesPopup.tsx | 219 ++++++++++++++++++------------- 1 file changed, 131 insertions(+), 88 deletions(-) diff --git a/src/features/dailyNotesPopup.tsx b/src/features/dailyNotesPopup.tsx index 5e2342a..dd0dc7d 100644 --- a/src/features/dailyNotesPopup.tsx +++ b/src/features/dailyNotesPopup.tsx @@ -11,8 +11,8 @@ import getUids from "roamjs-components/dom/getUids"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import type { OnloadArgs } from "roamjs-components/types"; import { addCommand } from "./workBench"; +import createHTMLObserver from "roamjs-components/dom/createHTMLObserver"; -let observerHeadings: MutationObserver | undefined = undefined; let closeDailyNotesPopup: (() => void) | undefined; export const moveForwardToDate = (bForward: boolean) => { @@ -543,96 +543,139 @@ const jumpDateIcon = () => { return true; }; +// Daily Note Subtitles / Banner +const ROAM_TITLE_CLASS = "rm-title-display"; +const ROAM_TITLE_CONTAINER_CLASS = "rm-title-display-container"; +const DAY_BANNER_CLASS = "roam-title-day-banner"; +const MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +const WEEKDAYS = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +// Daily Note Subtitles / Banner - Utils +const parseDateFromHeading = (headingText: string): Date | null => { + const [month, date = "", year = ""] = headingText.split(" "); + const dateMatch = date.match(/^(\d{1,2})(st|nd|rd|th),$/); + + if (!year || !dateMatch || !MONTHS.includes(month)) { + return null; + } + + const pageDate = new Date( + Number(year), + MONTHS.indexOf(month), + Number(dateMatch[1]) + ); + return !isNaN(pageDate.valueOf()) ? pageDate : null; +}; +const createDayBanner = ( + dayOfWeek: number, + heading: HTMLHeadingElement +): HTMLDivElement => { + const banner = document.createElement("div"); + banner.className = DAY_BANNER_CLASS; + banner.innerText = WEEKDAYS[dayOfWeek]; + banner.style.fontSize = "10pt"; + banner.style.position = "relative"; + + // Calculate positioning based on heading's margin + const headingMargin = getComputedStyle(heading).marginBottom; + const marginValue = Number(headingMargin.replace("px", "")) || 0; + banner.style.top = `-${marginValue + 6}px`; + + return banner; +}; +const insertBanner = ( + banner: HTMLDivElement, + heading: HTMLHeadingElement +): void => { + const container = heading.closest(`.${ROAM_TITLE_CONTAINER_CLASS}`); + const insertionPoint = container || heading; + insertionPoint.insertAdjacentElement("afterend", banner); +}; +const hasExistingBanner = (heading: HTMLHeadingElement): boolean => { + // Check DNP case: banner is next sibling of heading + const nextSibling = heading.nextElementSibling; + if (nextSibling && nextSibling.classList.contains(DAY_BANNER_CLASS)) { + return true; + } + + // Check page/sidebar case: banner is next sibling of container + const container = heading.closest(`.${ROAM_TITLE_CONTAINER_CLASS}`); + if (container) { + const containerNextSibling = container.nextElementSibling; + if ( + containerNextSibling && + containerNextSibling.classList.contains(DAY_BANNER_CLASS) + ) { + return true; + } + } + + return false; +}; +const addDateToRoamTitleBanner = (heading: HTMLHeadingElement): void => { + if (hasExistingBanner(heading)) return; + + const pageDate = parseDateFromHeading(heading.innerText); + if (!pageDate) return; + + const dayOfWeek = pageDate.getDay(); + const banner = createDayBanner(dayOfWeek, heading); + insertBanner(banner, heading); +}; +const processExistingHeadings = (): void => { + const existingHeadings = document.querySelectorAll(`.${ROAM_TITLE_CLASS}`); + existingHeadings.forEach((heading) => { + addDateToRoamTitleBanner(heading as HTMLHeadingElement); + }); +}; + +// Daily Note Subtitles / Banner - Observer +let observerHeadings: { disconnect: () => void } | undefined = undefined; +const setupHeadingObserver = (): void => { + observerHeadings = createHTMLObserver({ + tag: "H1", + className: ROAM_TITLE_CLASS, + callback: (element) => + addDateToRoamTitleBanner(element as HTMLHeadingElement), + }); +}; +const cleanupHeadingObserver = (): void => { + if (observerHeadings) { + observerHeadings.disconnect(); + observerHeadings = undefined; + } +}; + +// Main Daily Notes Popup Component export const component = { async initialize() { const setting = get("dailySubtitles"); - if (setting != "off") { - // TODO - Move This - const MONTHS = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; - const addDateToRoamTitleBanners = (titles: HTMLHeadingElement[]) => { - titles.forEach((title) => { - if ( - title.nextElementSibling && - title.nextElementSibling.classList.contains("roam-title-day-banner") - ) { - return; - } - const [month, date = "", year = ""] = title.innerText.split(" "); - const dateMatch = date.match(/^(\d{1,2})(st|nd|rd|th),$/); - const pageDate = - year && - dateMatch && - MONTHS.includes(month) && - new Date(Number(year), MONTHS.indexOf(month), Number(dateMatch[1])); - if (pageDate && !isNaN(pageDate.valueOf())) { - var weekdays = new Array( - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday" - ); - var day = pageDate.getDay(); - var div = document.createElement("DIV"); - div.className = "roam-title-day-banner"; - div.innerText = weekdays[day]; - div.style.fontSize = "10pt"; - div.style.top = - -( - Number(getComputedStyle(title).marginBottom.replace("px", "")) + - 6 - ) + "px"; - div.style.position = "relative"; - title.insertAdjacentElement("afterend", div); - } - }); - }; + if (setting === "off") return; - const className = "rm-title-display"; - addDateToRoamTitleBanners( - Array.from(document.querySelectorAll(`.${className}`)) - ); - observerHeadings = new MutationObserver((ms) => { - const titles = ms - .flatMap((m) => - Array.from(m.addedNodes).filter( - (d) => - /^H\d$/.test(d.nodeName) && - (d as Element).classList.contains(className) - ) - ) - .concat( - ms.flatMap((m) => - Array.from(m.addedNodes) - .filter((n) => n.hasChildNodes()) - .flatMap((d) => - Array.from((d as Element).getElementsByClassName(className)) - ) - ) - ) - .map((n) => n as HTMLHeadingElement); - addDateToRoamTitleBanners(titles); - }); - observerHeadings.observe(document.body, { - childList: true, - subtree: true, - }); - } + processExistingHeadings(); + setupHeadingObserver(); }, saveUIChanges(UIValues: { @@ -719,7 +762,7 @@ export const toggleFeature = ( ); component.initialize(); } else { - observerHeadings?.disconnect(); + cleanupHeadingObserver(); unloads.forEach((u) => u()); unloads.clear(); } From c95ce8621f4c896b953e78b0b1ec3f4e298e07db Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Wed, 1 Oct 2025 16:30:13 -0600 Subject: [PATCH 2/3] 1.7.1 --- 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 7ec1339..510c1ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "workbench", - "version": "1.7.0", + "version": "1.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "workbench", - "version": "1.7.0", + "version": "1.7.1", "dependencies": { "@mozilla/readability": "^0.3.0", "buffer": "^6.0.3", diff --git a/package.json b/package.json index 9dde204..9dddb41 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.0", + "version": "1.7.1", "samepage": { "extends": "node_modules/roamjs-components/package.json" } From 1b96405d90bbb0fdb1c32ede5126a1f9cd081eee Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Thu, 2 Oct 2025 15:09:12 -0600 Subject: [PATCH 3/3] rm DNP check --- src/features/dailyNotesPopup.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/features/dailyNotesPopup.tsx b/src/features/dailyNotesPopup.tsx index dd0dc7d..a1d8d7e 100644 --- a/src/features/dailyNotesPopup.tsx +++ b/src/features/dailyNotesPopup.tsx @@ -614,13 +614,7 @@ const insertBanner = ( insertionPoint.insertAdjacentElement("afterend", banner); }; const hasExistingBanner = (heading: HTMLHeadingElement): boolean => { - // Check DNP case: banner is next sibling of heading - const nextSibling = heading.nextElementSibling; - if (nextSibling && nextSibling.classList.contains(DAY_BANNER_CLASS)) { - return true; - } - - // Check page/sidebar case: banner is next sibling of container + // Check if banner is next sibling of container (works for all instances now) const container = heading.closest(`.${ROAM_TITLE_CONTAINER_CLASS}`); if (container) { const containerNextSibling = container.nextElementSibling;