Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates EditorContent to support adding anchor tags around headers for easy navigation. #1253

Merged
Show file tree
Hide file tree
Changes from 10 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
10 changes: 10 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ const config = args => {
},
plugins,
},
{
input: "./src/components/EditorContent/navigableHeadings.js",
output: {
dir: `${__dirname}/dist`,
format: "cjs",
sourcemap: true,
assetFileNames: "[name][extname]",
},
plugins,
},
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export default CodeBlockLowlight.extend({
highlightedLines: {
default: [],
parseHTML: element =>
element.dataset.highlightedLines?.split(",").map(Number) || [],
element.dataset.highlightedLines
?.split(",")
.filter(Boolean)
.map(Number) || [],
renderHTML: attributes => ({
"data-highlighted-lines":
attributes.highlightedLines?.join(",") ?? "",
Expand Down
4 changes: 4 additions & 0 deletions src/components/EditorContent/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ export const VARIABLE_SPAN_REGEX =
/<span data-variable="" [^>]*data-label="([^"]+)">{{([^}]+)}}<\/span>/g;

export const LANGUAGE_LIST = [...lowlight.listLanguages(), "html"];

export const EDITOR_CONTENT_DEFAULT_CONFIGURATION = {
navigableHeader: false,
};
17 changes: 14 additions & 3 deletions src/components/EditorContent/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,34 @@ import { createRoot } from "react-dom/client";
import { EDITOR_SIZES } from "src/common/constants";
import "src/styles/editor/editor-content.scss";

import { EDITOR_CONTENT_CLASS_NAME, SANITIZE_OPTIONS } from "./constants";
import {
EDITOR_CONTENT_CLASS_NAME,
EDITOR_CONTENT_DEFAULT_CONFIGURATION,
SANITIZE_OPTIONS,
} from "./constants";
import ImagePreview from "./ImagePreview";
import {
highlightCode,
substituteVariables,
applyLineHighlighting,
applySyntaxHighlighting,
} from "./utils";
import { makeHeadingsNavigable } from "./utils/headers";

const EditorContent = ({
content = "",
variables = [],
className,
size = EDITOR_SIZES.MEDIUM,
configuration = EDITOR_CONTENT_DEFAULT_CONFIGURATION,
...otherProps
}) => {
const [imagePreviewDetails, setImagePreviewDetails] = useState(null);
const editorContentRef = useRef(null);

const htmlContent = substituteVariables(highlightCode(content), variables);
const htmlContent = substituteVariables(
applySyntaxHighlighting(content),
variables
);
const sanitize = DOMPurify.sanitize;

const injectCopyButtonToCodeBlocks = () => {
Expand Down Expand Up @@ -72,6 +81,8 @@ const EditorContent = ({
injectCopyButtonToCodeBlocks();
bindImageClickListener();
applyLineHighlighting(editorContentRef.current);
configuration.navigableHeader &&
makeHeadingsNavigable(editorContentRef.current);
}, [content]);

return (
Expand Down
7 changes: 7 additions & 0 deletions src/components/EditorContent/navigableHeadings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { makeHeadingsNavigable } from "./utils/headers";

document.addEventListener("DOMContentLoaded", () => {
const editorContent = document.querySelector(".neeto-editor-content");

if (editorContent) makeHeadingsNavigable(editorContent);
});
45 changes: 45 additions & 0 deletions src/components/EditorContent/utils/headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const buildLinkSVG = () => {
const svgNS = "http://www.w3.org/2000/svg";

const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("height", "20");
svg.setAttribute("viewBox", "0 0 16 16");
svg.setAttribute("width", "20");

const path = document.createElementNS(svgNS, "path");
path.setAttribute(
"d",
"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"
);

svg.appendChild(path);

return svg;
};

const convertTextToId = text =>
text
.trim()
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.replace(/\s+/g, "-");

export const makeHeadingsNavigable = editorContentNode => {
const headerTags = editorContentNode.querySelectorAll(
"h1, h2, h3, h4, h5, h6"
);

headerTags.forEach(heading => {
const headingId = convertTextToId(heading.textContent);
heading.setAttribute("id", headingId);

const anchor = document.createElement("a");
anchor.setAttribute("href", `#${headingId}`);
anchor.classList.add("header-wrapper-link");
anchor.appendChild(buildLinkSVG());
anchor.appendChild(heading.cloneNode(true));

heading.replaceWith(anchor);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
CODE_BLOCK_REGEX,
LANGUAGE_LIST,
VARIABLE_SPAN_REGEX,
} from "./constants";
} from "../constants";

const transformCode = code =>
code.replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
Expand Down Expand Up @@ -62,7 +62,7 @@ export const highlightLinesElement = (code, options) => {
highlightLinesCode(code, options);
};

export const highlightCode = content => {
export const applySyntaxHighlighting = content => {
lowlight.highlightAuto("");
let highlightedAST = {};

Expand Down Expand Up @@ -97,7 +97,10 @@ export const applyLineHighlighting = editorContent => {
.closest("pre")
.getAttribute("data-highlighted-lines");
if (highlightedLines) {
const linesToHighlight = highlightedLines.split(",")?.map(Number);
const linesToHighlight = highlightedLines
.split(",")
?.filter(Boolean)
.map(Number);

const highlightLinesOptions = linesToHighlight.map(line => ({
start: line,
Expand Down
23 changes: 22 additions & 1 deletion src/styles/editor/editor-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,27 @@
margin: var(--neeto-editor-content-h6-margin);
}

// header anchor tags
.header-wrapper-link {
position: relative;
h1, h2, h3, h4, h5, h6 {
cursor: pointer;
}

svg {
position: absolute;
left: -25px;
height: 100%;
visibility: hidden;
opacity: 0;
transition: opacity 150ms ease-in;
}
&:hover svg{
visibility: visible;
opacity: 1;
}
}

// Paragraph
p {
font-size: var(--neeto-editor-content-paragraph-font-size);
Expand Down Expand Up @@ -530,4 +551,4 @@
background-color: rgb(var(--neeto-editor-gray-300));
}
}
}
}
11 changes: 9 additions & 2 deletions stories/Walkthroughs/Output/EditorContentDemo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ const EditorContentDemo = () => {
<div className="neeto-ui-flex neeto-ui-flex-col neeto-ui-gap-4">
<div
className="neeto-ui-border-gray-400 neeto-ui-rounded neeto-ui-border"
style={{ border: "1px solid rgb(var(--neeto-ui-gray-400))" }}
style={{
border: "1px solid rgb(var(--neeto-ui-gray-400))",
padding: "25px",
}}
>
<EditorContent {...{ content }} className="neeto-ui-p-4" />
<EditorContent
{...{ content }}
className="neeto-ui-p-4"
configuration={{ navigableHeader: true }}
/>
</div>
<div>
<h3>Editor</h3>
Expand Down
6 changes: 5 additions & 1 deletion stories/Walkthroughs/Output/constants.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ interface AttachmentsProps {
allowDelete?: boolean;
}

type EditorContentConfigType = {
navigableHeader?: boolean;
}

export function Editor(props: EditorProps): JSX.Element;

export function FormikEditor(props: FormikEditorProps): JSX.Element;
Expand All @@ -160,6 +164,7 @@ export function EditorContent(props: {
className?: string;
variables?: (VariableCategory | Variable)[];
size?: "large" | "medium";
configuration?: EditorContentConfigType;
[otherProps: string]: any;
}): JSX.Element;

Expand Down