diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx
index d253070827..f861641708 100644
--- a/plugins/techdocs/src/reader/components/Reader.tsx
+++ b/plugins/techdocs/src/reader/components/Reader.tsx
@@ -28,7 +28,6 @@ import {
import { Alert } from '@material-ui/lab';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
-import { useAsync } from 'react-use';
import { techdocsStorageApiRef } from '../../api';
import {
addBaseUrl,
@@ -110,32 +109,26 @@ export const Reader = ({ entityId, onReady }: Props) => {
// an update to "state" might lead to an updated UI so we include it as a trigger
}, [updateSidebarPosition, state]);
- useAsync(async () => {
- if (!rawPage || !shadowDomRef.current) {
- return;
- }
- if (onReady) {
- onReady();
- }
-
- // Pre-render
- const transformedElement = await transformer(rawPage, [
- sanitizeDOM(),
- addBaseUrl({
- techdocsStorageApi,
- entityId: {
- kind,
- name,
- namespace,
- },
- path,
- }),
- rewriteDocLinks(),
- removeMkdocsHeader(),
- simplifyMkdocsFooter(),
- addGitFeedbackLink(scmIntegrationsApi),
- injectCss({
- css: `
+ // a function that performs transformations that are executed prior to adding it to the DOM
+ const preRender = useCallback(
+ (rawContent: string, contentPath: string) =>
+ transformer(rawContent, [
+ sanitizeDOM(),
+ addBaseUrl({
+ techdocsStorageApi,
+ entityId: {
+ kind,
+ name,
+ namespace,
+ },
+ path: contentPath,
+ }),
+ rewriteDocLinks(),
+ removeMkdocsHeader(),
+ simplifyMkdocsFooter(),
+ addGitFeedbackLink(scmIntegrationsApi),
+ injectCss({
+ css: `
body {
font-family: ${theme.typography.fontFamily};
--md-text-color: ${theme.palette.text.primary};
@@ -192,21 +185,21 @@ export const Reader = ({ entityId, onReady }: Props) => {
}
}
`,
- }),
- injectCss({
- // Disable CSS animations on link colors as they lead to issues in dark
- // mode. The dark mode color theme is applied later and theirfore there
- // is always an animation from light to dark mode when navigation
- // between pages.
- css: `
+ }),
+ injectCss({
+ // Disable CSS animations on link colors as they lead to issues in dark
+ // mode. The dark mode color theme is applied later and theirfore there
+ // is always an animation from light to dark mode when navigation
+ // between pages.
+ css: `
.md-nav__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
transition: none;
}
`,
- }),
- injectCss({
- // Properly style code blocks.
- css: `
+ }),
+ injectCss({
+ // Properly style code blocks.
+ css: `
.md-typeset pre > code::-webkit-scrollbar-thumb {
background-color: hsla(0, 0%, 0%, 0.32);
}
@@ -214,17 +207,17 @@ export const Reader = ({ entityId, onReady }: Props) => {
background-color: hsla(0, 0%, 0%, 0.87);
}
`,
- }),
- injectCss({
- // Admonitions and others are using SVG masks to define icons. These
- // masks are defined as CSS variables.
- // As the MkDocs output is rendered in shadow DOM, the CSS variable
- // definitions on the root selector are not applied. Instead, the have
- // to be applied on :host.
- // As there is no way to transform the served main*.css yet (for
- // example in the backend), we have to copy from main*.css and modify
- // them.
- css: `
+ }),
+ injectCss({
+ // Admonitions and others are using SVG masks to define icons. These
+ // masks are defined as CSS variables.
+ // As the MkDocs output is rendered in shadow DOM, the CSS variable
+ // definitions on the root selector are not applied. Instead, the have
+ // to be applied on :host.
+ // As there is no way to transform the served main*.css yet (for
+ // example in the backend), we have to copy from main*.css and modify
+ // them.
+ css: `
:host {
--md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,');
--md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,');
@@ -250,97 +243,125 @@ export const Reader = ({ entityId, onReady }: Props) => {
--md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,');
}
`,
- }),
- ]);
+ }),
+ ]),
+ [
+ kind,
+ name,
+ namespace,
+ scmIntegrationsApi,
+ techdocsStorageApi,
+ theme.palette.background.default,
+ theme.palette.background.paper,
+ theme.palette.primary.main,
+ theme.palette.text.primary,
+ theme.typography.fontFamily,
+ ],
+ );
- if (!transformedElement?.innerHTML) {
- return; // An unexpected error occurred
+ // a function that performs transformations that are executed after adding it to the DOM
+ const postRender = useCallback(
+ async (shadowRoot: ShadowRoot) =>
+ transformer(shadowRoot.children[0], [
+ dom => {
+ setTimeout(() => {
+ // Scoll to the desired anchor on initial navigation
+ if (window.location.hash) {
+ const hash = window.location.hash.slice(1);
+ shadowRoot?.getElementById(hash)?.scrollIntoView();
+ }
+ }, 200);
+ return dom;
+ },
+ addLinkClickListener({
+ baseUrl: window.location.origin,
+ onClick: (_: MouseEvent, url: string) => {
+ const parsedUrl = new URL(url);
+
+ if (parsedUrl.hash) {
+ navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
+
+ // Scroll to hash if it's on the current page
+ shadowRoot
+ ?.getElementById(parsedUrl.hash.slice(1))
+ ?.scrollIntoView();
+ } else {
+ navigate(parsedUrl.pathname);
+ }
+ },
+ }),
+ onCssReady({
+ docStorageUrl: await techdocsStorageApi.getApiOrigin(),
+ onLoading: (dom: Element) => {
+ (dom as HTMLElement).style.setProperty('opacity', '0');
+ },
+ onLoaded: (dom: Element) => {
+ (dom as HTMLElement).style.removeProperty('opacity');
+ // disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
+ (dom as HTMLElement)
+ .querySelector('.md-nav__title')
+ ?.removeAttribute('for');
+ const sideDivs: HTMLElement[] = Array.from(
+ shadowRoot!.querySelectorAll('.md-sidebar'),
+ );
+ setSidebars(sideDivs);
+ // set sidebar height so they don't initially render in wrong position
+ const docTopPosition = (dom as HTMLElement).getBoundingClientRect()
+ .top;
+ const mdTabs = dom.querySelector('.md-container > .md-tabs');
+ sideDivs!.forEach(sidebar => {
+ sidebar.style.top = mdTabs
+ ? `${docTopPosition + mdTabs.getBoundingClientRect().height}px`
+ : `${docTopPosition}px`;
+ });
+ },
+ }),
+ ]),
+ [navigate, techdocsStorageApi],
+ );
+
+ useEffect(() => {
+ if (!rawPage || !shadowDomRef.current) {
+ return () => {};
+ }
+ if (onReady) {
+ onReady();
}
- const shadowDiv: HTMLElement = shadowDomRef.current!;
- const shadowRoot =
- shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
- Array.from(shadowRoot.children).forEach(child =>
- shadowRoot.removeChild(child),
- );
- shadowRoot.appendChild(transformedElement);
+ // if false, there is already a newer execution of this effect
+ let shouldReplaceContent = true;
- // Scroll to top after render
- window.scroll({ top: 0 });
+ // Pre-render
+ preRender(rawPage, path).then(async transformedElement => {
+ if (!transformedElement?.innerHTML) {
+ return; // An unexpected error occurred
+ }
- // Post-render
- await transformer(shadowRoot.children[0], [
- dom => {
- setTimeout(() => {
- // Scoll to the desired anchor on initial navigation
- if (window.location.hash) {
- const hash = window.location.hash.slice(1);
- shadowRoot?.getElementById(hash)?.scrollIntoView();
- }
- }, 200);
- return dom;
- },
- addLinkClickListener({
- baseUrl: window.location.origin,
- onClick: (_: MouseEvent, url: string) => {
- const parsedUrl = new URL(url);
+ // don't manipulate the shadow dom if this isn't the latest effect execution
+ if (!shouldReplaceContent) {
+ return;
+ }
- if (parsedUrl.hash) {
- navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
+ const shadowDiv: HTMLElement = shadowDomRef.current!;
+ const shadowRoot =
+ shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
+ Array.from(shadowRoot.children).forEach(child =>
+ shadowRoot.removeChild(child),
+ );
+ shadowRoot.appendChild(transformedElement);
- // Scroll to hash if it's on the current page
- shadowRoot
- ?.getElementById(parsedUrl.hash.slice(1))
- ?.scrollIntoView();
- } else {
- navigate(parsedUrl.pathname);
- }
- },
- }),
- onCssReady({
- docStorageUrl: await techdocsStorageApi.getApiOrigin(),
- onLoading: (dom: Element) => {
- (dom as HTMLElement).style.setProperty('opacity', '0');
- },
- onLoaded: (dom: Element) => {
- (dom as HTMLElement).style.removeProperty('opacity');
- // disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
- (dom as HTMLElement)
- .querySelector('.md-nav__title')
- ?.removeAttribute('for');
- const sideDivs: HTMLElement[] = Array.from(
- shadowRoot!.querySelectorAll('.md-sidebar'),
- );
- setSidebars(sideDivs);
- // set sidebar height so they don't initially render in wrong position
- const docTopPosition = (dom as HTMLElement).getBoundingClientRect()
- .top;
- const mdTabs = dom.querySelector('.md-container > .md-tabs');
- sideDivs!.forEach(sidebar => {
- sidebar.style.top = mdTabs
- ? `${docTopPosition + mdTabs.getBoundingClientRect().height}px`
- : `${docTopPosition}px`;
- });
- },
- }),
- ]);
- }, [
- path,
- kind,
- namespace,
- name,
- rawPage,
- navigate,
- onReady,
- shadowDomRef,
- techdocsStorageApi,
- theme.typography.fontFamily,
- theme.palette.text.primary,
- theme.palette.primary.main,
- theme.palette.background.paper,
- theme.palette.background.default,
- scmIntegrationsApi,
- ]);
+ // Scroll to top after render
+ window.scroll({ top: 0 });
+
+ // Post-render
+ await postRender(shadowRoot);
+ });
+
+ // cancel this execution
+ return () => {
+ shouldReplaceContent = false;
+ };
+ }, [onReady, path, postRender, preRender, rawPage]);
return (
<>