@backstage/plugin-techdocs
Version:
The Backstage plugin that renders technical documentation for your components
228 lines (225 loc) • 8.88 kB
JavaScript
import { useState, useCallback, useEffect, useLayoutEffect } from 'react';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { useTheme } from '@material-ui/core/styles';
import { useAnalytics, useApi, configApiRef } from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { techdocsStorageApiRef, useShadowDomStylesLoading } from '@backstage/plugin-techdocs-react';
import { useTechDocsReader } from '../TechDocsReaderProvider.esm.js';
import { useSanitizerTransformer } from '../../transformers/html/transformer.esm.js';
import { useStylesTransformer } from '../../transformers/styles/transformer.esm.js';
import { addBaseUrl } from '../../transformers/addBaseUrl.esm.js';
import { addGitFeedbackLink } from '../../transformers/addGitFeedbackLink.esm.js';
import { addSidebarToggle } from '../../transformers/addSidebarToggle.esm.js';
import { rewriteDocLinks } from '../../transformers/rewriteDocLinks.esm.js';
import { addLinkClickListener } from '../../transformers/addLinkClickListener.esm.js';
import { copyToClipboard } from '../../transformers/copyToClipboard.esm.js';
import { removeMkdocsHeader } from '../../transformers/removeMkdocsHeader.esm.js';
import { simplifyMkdocsFooter } from '../../transformers/simplifyMkdocsFooter.esm.js';
import { onCssReady } from '../../transformers/onCssReady.esm.js';
import { scrollIntoNavigation } from '../../transformers/scrollIntoNavigation.esm.js';
import { transform } from '../../transformers/transformer.esm.js';
import { handleMetaRedirects } from '../../transformers/handleMetaRedirects.esm.js';
import { addNavLinkKeyboardToggle } from '../../transformers/addNavLinkKeyboardToggle.esm.js';
import { useNavigateUrl } from './useNavigateUrl.esm.js';
import { useParams, useLocation, useNavigate } from 'react-router-dom';
const MOBILE_MEDIA_QUERY = "screen and (max-width: 76.1875em)";
const useInitialRedirect = (defaultPath) => {
const location = useLocation();
const navigate = useNavigate();
const { "*": currPath = "" } = useParams();
useLayoutEffect(() => {
if (currPath === "" && defaultPath) {
navigate(`${location.pathname}${defaultPath}`, { replace: true });
}
}, []);
};
const useTechDocsReaderDom = (entityRef, defaultPath) => {
const navigate = useNavigateUrl();
const theme = useTheme();
const isMobileMedia = useMediaQuery(MOBILE_MEDIA_QUERY);
const sanitizerTransformer = useSanitizerTransformer();
const stylesTransformer = useStylesTransformer();
const analytics = useAnalytics();
const techdocsStorageApi = useApi(techdocsStorageApiRef);
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
const configApi = useApi(configApiRef);
const { state, path, content: rawPage } = useTechDocsReader();
const { "*": currPath = "" } = useParams();
const [dom, setDom] = useState(null);
const isStyleLoading = useShadowDomStylesLoading(dom);
useInitialRedirect(defaultPath);
const updateSidebarPositionAndHeight = useCallback(() => {
if (!dom) return;
const sidebars = dom.querySelectorAll(".md-sidebar");
sidebars.forEach((element) => {
if (isMobileMedia) {
element.style.top = "0px";
} else {
const page = document?.querySelector(".techdocs-reader-page");
const pageTop = page?.getBoundingClientRect().top ?? 0;
let domTop = dom.getBoundingClientRect().top ?? 0;
const tabs = dom.querySelector(".md-container > .md-tabs");
const tabsHeight = tabs?.getBoundingClientRect().height ?? 0;
if (domTop < pageTop) {
domTop = pageTop;
}
const scrollbarTopPx = Math.max(domTop, 0) + tabsHeight;
element.style.top = `${scrollbarTopPx}px`;
const footer = dom.querySelector(".md-container > .md-footer");
const scrollbarEndPx = footer?.getBoundingClientRect().top ?? window.innerHeight;
element.style.height = `${scrollbarEndPx - scrollbarTopPx}px`;
}
element.style.setProperty("opacity", "1");
});
}, [dom, isMobileMedia]);
useEffect(() => {
window.addEventListener("resize", updateSidebarPositionAndHeight);
window.addEventListener("scroll", updateSidebarPositionAndHeight, true);
return () => {
window.removeEventListener("resize", updateSidebarPositionAndHeight);
window.removeEventListener(
"scroll",
updateSidebarPositionAndHeight,
true
);
};
}, [dom, updateSidebarPositionAndHeight]);
const updateFooterWidth = useCallback(() => {
if (!dom) return;
const footer = dom.querySelector(".md-footer");
if (footer) {
footer.style.width = `${dom.getBoundingClientRect().width}px`;
}
}, [dom]);
useEffect(() => {
window.addEventListener("resize", updateFooterWidth);
return () => {
window.removeEventListener("resize", updateFooterWidth);
};
}, [dom, updateFooterWidth]);
useEffect(() => {
if (!isStyleLoading) {
updateFooterWidth();
updateSidebarPositionAndHeight();
}
}, [
state,
isStyleLoading,
updateFooterWidth,
updateSidebarPositionAndHeight
]);
const preRender = useCallback(
(rawContent, contentPath) => transform(rawContent, [
sanitizerTransformer,
addBaseUrl({
techdocsStorageApi,
entityId: entityRef,
path: contentPath
}),
rewriteDocLinks(),
addSidebarToggle(),
removeMkdocsHeader(),
simplifyMkdocsFooter(),
addGitFeedbackLink(scmIntegrationsApi),
stylesTransformer
]),
[
// only add dependencies that are in state or memorized variables to avoid unnecessary calls between re-renders
entityRef,
scmIntegrationsApi,
techdocsStorageApi,
sanitizerTransformer,
stylesTransformer
]
);
const postRender = useCallback(
async (transformedElement) => transform(transformedElement, [
handleMetaRedirects(navigate, entityRef.name),
scrollIntoNavigation(),
copyToClipboard(theme),
addLinkClickListener({
baseUrl: configApi.getOptionalString("app.baseUrl") || window.location.origin,
onClick: (event, url) => {
const modifierActive = event.ctrlKey || event.metaKey;
const parsedUrl = new URL(url);
const linkText = event.target?.innerText || url;
const to = url.replace(window.location.origin, "");
analytics.captureEvent("click", linkText, { attributes: { to } });
if (parsedUrl.hash) {
if (modifierActive) {
window.open(url, "_blank");
} else {
if (window.location.pathname !== parsedUrl.pathname) {
navigate(url);
} else {
window.history.pushState(
null,
document.title,
parsedUrl.hash
);
}
transformedElement?.querySelector(`[id="${parsedUrl.hash.slice(1)}"]`)?.scrollIntoView();
transformedElement?.querySelector(`[id="${parsedUrl.hash.slice(1)}"]`)?.querySelector("a, button, [tabindex]")?.focus();
}
} else {
if (modifierActive) {
window.open(url, "_blank");
} else {
navigate(url);
}
}
}
}),
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
onCssReady({
onLoading: () => {
},
onLoaded: () => {
transformedElement.querySelector(".md-nav__title")?.removeAttribute("for");
}
}),
// hide sidebars until their positions are updated
onCssReady({
onLoading: () => {
const sidebars = Array.from(
transformedElement.querySelectorAll(".md-sidebar")
);
sidebars.forEach((element) => {
element.style.setProperty("opacity", "0");
});
},
onLoaded: () => {
}
}),
addNavLinkKeyboardToggle()
]),
[theme, navigate, analytics, entityRef.name, configApi]
);
useEffect(() => {
if (!rawPage) return () => {
};
let shouldReplaceContent = true;
preRender(rawPage, path).then(async (preTransformedDomElement) => {
if (!preTransformedDomElement?.innerHTML) {
return;
}
if (!shouldReplaceContent) {
return;
}
if (currPath !== path) {
return;
}
window.scroll({ top: 0 });
const postTransformedDomElement = await postRender(
preTransformedDomElement
);
setDom(postTransformedDomElement);
});
return () => {
shouldReplaceContent = false;
};
}, [rawPage, currPath, path, preRender, postRender]);
return dom;
};
export { useInitialRedirect, useTechDocsReaderDom };
//# sourceMappingURL=dom.esm.js.map