From 6d325f56e17605477449527f496d5b9e5b4eac41 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 7 May 2019 10:11:26 +0200 Subject: [PATCH 1/5] adds option to render markdown headings with anchor links --- .../src/MarkdownHeadingRenderer.js | 35 +++++++++++++++++++ .../src/MarkdownHeadingRenderer.test.js | 18 ++++++++++ .../ui-components/src/MarkdownView.js | 16 ++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js create mode 100644 scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js new file mode 100644 index 0000000000..d7268c2861 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js @@ -0,0 +1,35 @@ +// @flow +import * as React from "react"; + +/** + * Adds anchor links to markdown headings. + * + * @see Headings are missing anchors / ids + */ + +type Props = { + children: React.Node, + level: number +}; + +function flatten(text: string, child: any) { + return typeof child === "string" + ? text + child + : React.Children.toArray(child.props.children).reduce(flatten, text); +} + +/** + * Turns heading text into a anchor id + * + * @VisibleForTesting + */ +export function headingToAnchorId(heading: string) { + return heading.toLowerCase().replace(/\W/g, "-"); +} + +export default function MarkdownHeadingRenderer(props: Props) { + const children = React.Children.toArray(props.children); + const heading = children.reduce(flatten, ""); + const anchorId = headingToAnchorId(heading); + return React.createElement("h" + props.level, {id: anchorId}, props.children); +} diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js new file mode 100644 index 0000000000..4fd8428e98 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.test.js @@ -0,0 +1,18 @@ +// @flow +import React from "react"; +import { headingToAnchorId } from "./MarkdownHeadingRenderer"; + +describe("headingToAnchorId tests", () => { + + it("should lower case the text", () => { + expect(headingToAnchorId("Hello")).toBe("hello"); + expect(headingToAnchorId("HeLlO")).toBe("hello"); + expect(headingToAnchorId("HELLO")).toBe("hello"); + }); + + it("should replace spaces with hyphen", () => { + expect(headingToAnchorId("awesome stuff")).toBe("awesome-stuff"); + expect(headingToAnchorId("a b c d e f")).toBe("a-b-c-d-e-f"); + }); + +}); diff --git a/scm-ui-components/packages/ui-components/src/MarkdownView.js b/scm-ui-components/packages/ui-components/src/MarkdownView.js index 4d2b2de92f..164c06b84a 100644 --- a/scm-ui-components/packages/ui-components/src/MarkdownView.js +++ b/scm-ui-components/packages/ui-components/src/MarkdownView.js @@ -3,17 +3,27 @@ import React from "react"; import SyntaxHighlighter from "./SyntaxHighlighter"; import Markdown from "react-markdown/with-html"; import {binder} from "@scm-manager/ui-extensions"; +import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; type Props = { content: string, renderContext?: Object, renderers?: Object, + enableAnchorHeadings: boolean }; class MarkdownView extends React.Component { + static defaultProps = { + enableAnchorHeadings: false + }; + + constructor(props: Props) { + super(props); + } + render() { - const {content, renderers, renderContext} = this.props; + const {content, renderers, renderContext, enableAnchorHeadings} = this.props; const rendererFactory = binder.getExtension("markdown-renderer-factory"); let rendererList = renderers; @@ -26,6 +36,10 @@ class MarkdownView extends React.Component { rendererList = {}; } + if (enableAnchorHeadings) { + rendererList.heading = MarkdownHeadingRenderer; + } + if (!rendererList.code){ rendererList.code = SyntaxHighlighter; } From f99e685eea459857acc7a02b8f9b6a81281208df Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 7 May 2019 16:41:28 +0200 Subject: [PATCH 2/5] fix scroll to anchor link on page reload --- scm-ui/src/containers/ScrollToTop.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/containers/ScrollToTop.js b/scm-ui/src/containers/ScrollToTop.js index d48ea6531a..77054b8463 100644 --- a/scm-ui/src/containers/ScrollToTop.js +++ b/scm-ui/src/containers/ScrollToTop.js @@ -11,7 +11,15 @@ type Props = { class ScrollToTop extends React.Component { componentDidUpdate(prevProps) { if (this.props.location.pathname !== prevProps.location.pathname) { - window.scrollTo(0, 0); + const hash = this.props.location.hash; + if (hash) { + const element = document.getElementById(hash.substring(1)); + if (element && element.scrollIntoView) { + element.scrollIntoView(); + } + } else { + window.scrollTo(0, 0); + } } } From 6f962ff4cebe2f309effbbd6651a1602f947f025 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 7 May 2019 16:42:04 +0200 Subject: [PATCH 3/5] create links for markdown headings with enabled anchor links --- .../src/MarkdownHeadingRenderer.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js index d7268c2861..eab4bab05a 100644 --- a/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js +++ b/scm-ui-components/packages/ui-components/src/MarkdownHeadingRenderer.js @@ -1,5 +1,7 @@ // @flow import * as React from "react"; +import { withRouter } from "react-router-dom"; +import { withContextPath } from "./urls"; /** * Adds anchor links to markdown headings. @@ -9,7 +11,8 @@ import * as React from "react"; type Props = { children: React.Node, - level: number + level: number, + location: any }; function flatten(text: string, child: any) { @@ -27,9 +30,18 @@ export function headingToAnchorId(heading: string) { return heading.toLowerCase().replace(/\W/g, "-"); } -export default function MarkdownHeadingRenderer(props: Props) { +function MarkdownHeadingRenderer(props: Props) { const children = React.Children.toArray(props.children); const heading = children.reduce(flatten, ""); const anchorId = headingToAnchorId(heading); - return React.createElement("h" + props.level, {id: anchorId}, props.children); + const headingElement = React.createElement("h" + props.level, {}, props.children); + const href = withContextPath(props.location.pathname + "#" + anchorId); + + return ( + + {headingElement} + + ); } + +export default withRouter(MarkdownHeadingRenderer); From 033c213cf223f9e83985cf67223872e97ceff9b5 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 21 May 2019 10:22:53 +0200 Subject: [PATCH 4/5] fix scrolling for markdown content, which is loaded asynchronous The code to find and scroll to the anchor is now moved from the ScrollToTop component to the MarkdownView. The anchor with the id from location hash, is searched after the MarkdownView and all its children finished rendering. --- .../ui-components/src/MarkdownView.js | 42 +++++++++++++++---- scm-ui/src/containers/ScrollToTop.js | 10 +---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/MarkdownView.js b/scm-ui-components/packages/ui-components/src/MarkdownView.js index 164c06b84a..4620004f59 100644 --- a/scm-ui-components/packages/ui-components/src/MarkdownView.js +++ b/scm-ui-components/packages/ui-components/src/MarkdownView.js @@ -4,12 +4,17 @@ import SyntaxHighlighter from "./SyntaxHighlighter"; import Markdown from "react-markdown/with-html"; import {binder} from "@scm-manager/ui-extensions"; import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; +import { withRouter } from "react-router-dom"; + type Props = { content: string, renderContext?: Object, renderers?: Object, - enableAnchorHeadings: boolean + enableAnchorHeadings: boolean, + + // context props + location: any }; class MarkdownView extends React.Component { @@ -18,10 +23,27 @@ class MarkdownView extends React.Component { enableAnchorHeadings: false }; + contentRef: ?HTMLDivElement; + constructor(props: Props) { super(props); } + componentDidUpdate() { + // we have to use componentDidUpdate, because we have to wait until all + // children are rendered and componentDidMount is called before the + // markdown content was rendered. + const hash = this.props.location.hash; + if (this.contentRef && hash) { + // we query only child elements, to avoid strange scrolling with multiple + // markdown elements on one page. + const element = this.contentRef.querySelector(hash); + if (element && element.scrollIntoView) { + element.scrollIntoView(); + } + } + } + render() { const {content, renderers, renderContext, enableAnchorHeadings} = this.props; @@ -45,15 +67,17 @@ class MarkdownView extends React.Component { } return ( - +
(this.contentRef = el)}> + +
); } } -export default MarkdownView; +export default withRouter(MarkdownView); diff --git a/scm-ui/src/containers/ScrollToTop.js b/scm-ui/src/containers/ScrollToTop.js index 77054b8463..d48ea6531a 100644 --- a/scm-ui/src/containers/ScrollToTop.js +++ b/scm-ui/src/containers/ScrollToTop.js @@ -11,15 +11,7 @@ type Props = { class ScrollToTop extends React.Component { componentDidUpdate(prevProps) { if (this.props.location.pathname !== prevProps.location.pathname) { - const hash = this.props.location.hash; - if (hash) { - const element = document.getElementById(hash.substring(1)); - if (element && element.scrollIntoView) { - element.scrollIntoView(); - } - } else { - window.scrollTo(0, 0); - } + window.scrollTo(0, 0); } } From e2d05fd0149e231d501aa59e6e91adca5c12e759 Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 21 May 2019 13:38:16 +0000 Subject: [PATCH 5/5] Close branch feature/markdown_anchor_links