Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/roam/src/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,15 @@
width: 100px;
justify-content: space-between;
}

.rm-select-menu__item--highlighted {
background-color: #e6f0f5;
}

.dg-popover .bp3-popover {
z-index: 10000;
}

.text-selection-popup {
z-index: 1000;
}
219 changes: 217 additions & 2 deletions apps/roam/src/utils/initializeObserversAndListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
previewPageRefHandler,
overlayPageRefHandler,
} from "~/utils/pageRefObserverHandlers";
import getDiscourseNodes from "~/utils/getDiscourseNodes";
import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes";
import { OnloadArgs } from "roamjs-components/types";
import refreshConfigTree from "~/utils/refreshConfigTree";
import { render as renderGraphOverviewExport } from "~/components/ExportDiscourseContext";
Expand All @@ -45,6 +45,16 @@ import {
} from "~/utils/renderTextSelectionPopup";
import { renderNodeTagPopupButton } from "./renderNodeTagPopup";

let discourseNodes: DiscourseNode[] = [];
let discourseTagSet: Set<string> = new Set();

const refreshDiscourseNodeCache = () => {
discourseNodes = getDiscourseNodes();
discourseTagSet = new Set(
discourseNodes.flatMap((n) => (n.tag ? [n.tag.toLowerCase()] : [])),
);
};

const debounce = (fn: () => void, delay = 250) => {
let timeout: number;
return () => {
Expand All @@ -67,6 +77,7 @@ export const initObservers = async ({
nodeCreationPopoverListener: EventListener;
};
}> => {
refreshDiscourseNodeCache();
const pageTitleObserver = createHTMLObserver({
tag: "H1",
className: "rm-title-display",
Expand Down Expand Up @@ -102,7 +113,208 @@ export const initObservers = async ({
className: "rm-page-ref--tag",
tag: "SPAN",
callback: (s: HTMLSpanElement) => {
renderNodeTagPopupButton(s, onloadArgs.extensionAPI);
const tag = s.getAttribute("data-tag");
if (tag) {
if (discourseTagSet.has(tag.toLowerCase())) {
renderNodeTagPopupButton(s, discourseNodes, onloadArgs.extensionAPI);
}
}
},
});

const tableTagObserver = createHTMLObserver({
tag: "TD",
className: "relative",
callback: (el: HTMLElement) => {
if (!(el instanceof HTMLTableCellElement)) return;

const td = el;
if (!td.hasAttribute("data-cell-content")) return;

const content = td.dataset.cellContent || "";
if (!content.includes("#")) return;

const discourseNodes = getDiscourseNodes();
const discourseTagSet = new Set(
discourseNodes.map((n) => n.tag?.toLowerCase()).filter(Boolean),
);

const existingTags = td.querySelectorAll(".rm-page-ref--tag");
existingTags.forEach((tag) => {
const tagName = tag.getAttribute("data-tag");
if (
tagName &&
discourseTagSet.has(tagName.toLowerCase()) &&
tag instanceof HTMLSpanElement
) {
tag.removeAttribute("data-attribute-button-rendered");
renderNodeTagPopupButton(tag, onloadArgs.extensionAPI);
}
});

const innerContainers = [
td.querySelector("a.rm-page-ref > span"),
td.querySelector("div.rm-block__input > span"),
td.querySelector("div.rm-block__input"),
td.querySelector("div.roamjs-query-embed span"),
].filter(Boolean);

innerContainers.forEach((innerSpan) => {
if (!innerSpan) return;

const formattedTagsInside =
innerSpan.querySelectorAll(".rm-page-ref--tag");
if (formattedTagsInside.length > 0) return;

const textContent = innerSpan.textContent || "";
const unformattedDiscourseTagsFound = [];

discourseNodes.forEach((node) => {
const tag = node.tag;
if (!tag) return;

const pattern = new RegExp(`#${tag}(?![\\w-])`, "i");
if (pattern.test(textContent)) {
const alreadyFormatted = innerSpan.querySelector(
`.rm-page-ref--tag[data-tag="${tag}"]`,
);
if (!alreadyFormatted) {
unformattedDiscourseTagsFound.push(tag);
}
}
});

if (unformattedDiscourseTagsFound.length === 0) return;

const originalHtml = innerSpan.innerHTML;
const newHtml = originalHtml.replace(/#([\w-]+)/g, (match, tagName) => {
if (
innerSpan.querySelector(`.rm-page-ref--tag[data-tag="${tagName}"]`)
) {
return match;
}

if (discourseTagSet.has(tagName.toLowerCase())) {
return `<span class="rm-page-ref rm-page-ref--tag" data-tag="${tagName}">${match}</span>`;
}

return match;
});

if (originalHtml !== newHtml) {
innerSpan.innerHTML = newHtml;

setTimeout(() => {
const newTags = innerSpan.querySelectorAll(
'.rm-page-ref--tag:not([data-attribute-button-rendered="true"])',
);

newTags.forEach((tag) => {
if (tag instanceof HTMLSpanElement) {
renderNodeTagPopupButton(tag, onloadArgs.extensionAPI);
}
});
}, 50);
}
});
},
});

const tableTagObserver = createHTMLObserver({
tag: "TD",
className: "relative",
callback: (el: HTMLElement) => {
if (!(el instanceof HTMLTableCellElement)) return;

const td = el;
if (!td.hasAttribute("data-cell-content")) return;

const content = td.dataset.cellContent || "";
if (!content.includes("#")) return;

const discourseNodes = getDiscourseNodes();
const discourseTagSet = new Set(
discourseNodes.map((n) => n.tag?.toLowerCase()).filter(Boolean),
);

const existingTags = td.querySelectorAll(".rm-page-ref--tag");
existingTags.forEach((tag) => {
const tagName = tag.getAttribute("data-tag");
if (
tagName &&
discourseTagSet.has(tagName.toLowerCase()) &&
tag instanceof HTMLSpanElement
) {
tag.removeAttribute("data-attribute-button-rendered");
renderNodeTagPopupButton(tag, onloadArgs.extensionAPI);
}
});

const innerContainers = [
td.querySelector("a.rm-page-ref > span"),
td.querySelector("div.rm-block__input > span"),
td.querySelector("div.rm-block__input"),
td.querySelector("div.roamjs-query-embed span"),
].filter(Boolean);

innerContainers.forEach((innerSpan) => {
if (!innerSpan) return;

const formattedTagsInside =
innerSpan.querySelectorAll(".rm-page-ref--tag");
if (formattedTagsInside.length > 0) return;

const textContent = innerSpan.textContent || "";
const unformattedDiscourseTagsFound = [];

discourseNodes.forEach((node) => {
const tag = node.tag;
if (!tag) return;

const pattern = new RegExp(`#${tag}(?![\\w-])`, "i");
if (pattern.test(textContent)) {
const alreadyFormatted = innerSpan.querySelector(
`.rm-page-ref--tag[data-tag="${tag}"]`,
);
if (!alreadyFormatted) {
unformattedDiscourseTagsFound.push(tag);
}
}
});

if (unformattedDiscourseTagsFound.length === 0) return;

const originalHtml = innerSpan.innerHTML;
const newHtml = originalHtml.replace(/#([\w-]+)/g, (match, tagName) => {
if (
innerSpan.querySelector(`.rm-page-ref--tag[data-tag="${tagName}"]`)
) {
return match;
}

if (discourseTagSet.has(tagName.toLowerCase())) {
return `<span class="rm-page-ref rm-page-ref--tag" data-tag="${tagName}">${match}</span>`;
}

return match;
});

if (originalHtml !== newHtml) {
innerSpan.innerHTML = newHtml;

setTimeout(() => {
const newTags = innerSpan.querySelectorAll(
'.rm-page-ref--tag:not([data-attribute-button-rendered="true"])',
);

newTags.forEach((tag) => {
if (tag instanceof HTMLSpanElement) {
renderNodeTagPopupButton(tag, onloadArgs.extensionAPI);
}
});
}, 50);
}
});
},
});

Expand Down Expand Up @@ -152,6 +364,7 @@ export const initObservers = async ({
});
// refresh config tree after config page is created
refreshConfigTree();
refreshDiscourseNodeCache();

const hashChangeListener = (e: Event) => {
const evt = e as HashChangeEvent;
Expand All @@ -162,6 +375,7 @@ export const initObservers = async ({
getDiscourseNodes().some(({ type }) => evt.oldURL.endsWith(type))
) {
refreshConfigTree();
refreshDiscourseNodeCache();
}
};

Expand Down Expand Up @@ -316,6 +530,7 @@ export const initObservers = async ({
linkedReferencesObserver,
graphOverviewExportObserver,
nodeTagPopupButtonObserver,
tableTagObserver,
].filter((o): o is MutationObserver => !!o),
listeners: {
pageActionListener,
Expand Down
Loading
Loading