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

🎉 Entry Emulator - sidebar table of contents #2869

Merged
merged 17 commits into from
Nov 6, 2023
Merged
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
1 change: 1 addition & 0 deletions adminSiteClient/gdocsDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const checkIsLightningUpdate = (
"cover-color": true,
"cover-image": true,
"hide-citation": true,
"sidebar-toc": true,
body: true,
dateline: true,
details: true,
Expand Down
1 change: 1 addition & 0 deletions baker/SiteBaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export class SiteBaker {

// Bake all GDoc posts
async bakeGDocPosts() {
await db.getConnection()
if (!this.bakeSteps.has("gdocPosts")) return
const publishedGdocs = await Gdoc.getPublishedGdocs()

Expand Down
1 change: 1 addition & 0 deletions db/migrateWpPostsToArchieMl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ const migrate = async (): Promise<void> => {
// Provide an empty array to prevent the sticky nav from rendering at all
// Because if it isn't defined, it tries to automatically populate itself
"sticky-nav": isEntry ? [] : undefined,
"sidebar-toc": isEntry,
},
relatedCharts,
published: false,
Expand Down
51 changes: 44 additions & 7 deletions db/model/Gdoc/archieToEnriched.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,31 +121,67 @@ function generateStickyNav(
}

function generateToc(
body: OwidEnrichedGdocBlock[]
body: OwidEnrichedGdocBlock[],
isTocForSidebar: boolean = false
): TocHeadingWithTitleSupertitle[] {
// For linear topic pages, we record h1s & h2s
// For the sdg-toc, we record h2s & h3s (as it was developed before we decided to use h1s as our top level heading)
// It would be nice to standardise this but it would require a migration, updating CSS, updating Gdocs, etc.
const [primary, secondary] = isTocForSidebar ? [1, 2] : [2, 3]
const toc: TocHeadingWithTitleSupertitle[] = []

// track h2s and h3s for the SDG table of contents
body.forEach((block) =>
traverseEnrichedBlocks(block, (child) => {
if (child.type === "heading") {
const { level, text, supertitle } = child
const titleString = spansToSimpleString(text)
const supertitleString =
supertitle && spansToSimpleString(supertitle)
if (titleString && (level === 2 || level === 3)) {
const supertitleString = supertitle
? spansToSimpleString(supertitle)
: ""
if (titleString && (level === primary || level === secondary)) {
toc.push({
title: titleString,
supertitle: supertitleString,
text: titleString,
slug: urlSlug(`${supertitleString} ${titleString}`),
Copy link
Member

@mlbrgl mlbrgl Nov 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came across two headings with the same title/slug in time-use ("Additional information"), which made them both light up at the same time in the ToC. We had a provision in WP code to handle this special case but it hasn't been ported over yet. It is not the most critical bug but it can be quite visible so I'm a bit on the fence as to whether to make the fix par of this PR.

Here is an example doc: https://docs.google.com/document/d/1uYZEcCP2shwa5U1EYaGb6cRvLFEz0MrUJv9m8UASbqs/edit

Screenshot 2023-10-31 at 18 44 52

isSubheading: level === 3,
isSubheading: level === secondary,
})
}
}
if (isTocForSidebar && child.type === "all-charts") {
toc.push({
title: child.heading,
text: child.heading,
slug: ALL_CHARTS_ID,
isSubheading: false,
})
}
})
)

if (isTocForSidebar) {
toc.push(
{
title: "Endnotes",
text: "Endnotes",
slug: "article-endnotes",
isSubheading: false,
},
{
title: "Citation",
text: "Citation",
slug: "article-citation",
isSubheading: false,
},
{
title: "Licence",
text: "Licence",
slug: "article-licence",
isSubheading: false,
}
)
}

return toc
}

Expand Down Expand Up @@ -255,7 +291,8 @@ export const archieToEnriched = (text: string): OwidGdocContent => {
// Parse elements of the ArchieML into enrichedBlocks
parsed.body = compact(parsed.body.map(parseRawBlocksToEnrichedBlocks))

parsed.toc = generateToc(parsed.body)
const isTocForSidebar = parsed["sidebar-toc"] === "true"
parsed.toc = generateToc(parsed.body, isTocForSidebar)

const parsedRefs = parseRefs({
refs: [...(parsed.refs ?? []), ...rawInlineRefs],
Expand Down
1 change: 1 addition & 0 deletions db/model/Gdoc/archieToGdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function* owidArticleToArchieMLStringGenerator(
}
yield "[]"
}
yield* propertyToArchieMLString("sidebar-toc", article)
// TODO: inline refs
yieldMultiBlockPropertyIfDefined("summary", article, article.summary)
yield* propertyToArchieMLString("hide-citation", article)
Expand Down
3 changes: 2 additions & 1 deletion db/model/Gdoc/rawToArchie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ import {
import { match } from "ts-pattern"

export function appendDotEndIfMultiline(
line: string | null | undefined
line: string | boolean | null | undefined
): string {
if (typeof line === "boolean") return line ? "true" : "false"
if (line && line.includes("\n")) return line + "\n:end"
return line ?? ""
}
Expand Down
1 change: 1 addition & 0 deletions packages/@ourworldindata/utils/src/owidTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,7 @@ export interface OwidGdocContent {
"featured-image"?: string
"atom-title"?: string
"atom-excerpt"?: string
"sidebar-toc"?: boolean
"cover-color"?:
| "sdg-color-1"
| "sdg-color-2"
Expand Down
43 changes: 38 additions & 5 deletions site/TableOfContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ interface TableOfContentsData {
headings: TocHeading[]
pageTitle: string
hideSubheadings?: boolean
headingLevels?: {
primary: number
secondary: number
}
}

const isRecordTopViewport = (record: IntersectionObserverEntry) => {
Expand All @@ -34,17 +38,36 @@ export const TableOfContents = ({
headings,
pageTitle,
hideSubheadings,
// Original WP articles used a hierarchy of h2 and h3 headings
// New Gdoc articles use a hierarchy of h1 and h2 headings
headingLevels = {
primary: 2,
secondary: 3,
},
}: TableOfContentsData) => {
const [isOpen, setIsOpen] = useState(false)
const [activeHeading, setActiveHeading] = useState("")
const { primary, secondary } = headingLevels
const tocRef = useRef<HTMLElement>(null)

const toggleIsOpen = () => {
setIsOpen(!isOpen)
}
// The Gdocs sidebar can't rely on the same CSS logic that old-style entries use, so we need to
// explicitly trigger these toggles based on screen width
const toggleIsOpenOnMobile = () => {
if (window.innerWidth < 1536) {
toggleIsOpen()
}
}

useTriggerWhenClickOutside(tocRef, isOpen, setIsOpen)

// Open the sidebar on desktop by default when mounting
useEffect(() => {
setIsOpen(window.innerWidth >= 1536)
}, [])

useEffect(() => {
if ("IntersectionObserver" in window) {
const previousHeadings = headings.map((heading, i) => ({
Expand Down Expand Up @@ -107,16 +130,26 @@ export const TableOfContents = ({
)

let contentHeadings = null
// In Gdocs articles, these sections are ID'd via unique elements
const appendixDivs =
", h3#article-endnotes, section#article-citation, section#article-licence"
if (hideSubheadings) {
contentHeadings = document.querySelectorAll("h2")
contentHeadings = document.querySelectorAll(
`h${secondary} ${appendixDivs}`
)
} else {
contentHeadings = document.querySelectorAll("h2, h3")
contentHeadings = document.querySelectorAll(
`h${primary}, h${secondary} ${appendixDivs}`
)
}
contentHeadings.forEach((contentHeading) => {
observer.observe(contentHeading)
})

return () => observer.disconnect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

}
}, [headings, hideSubheadings])
return
}, [headings, hideSubheadings, primary, secondary])

return (
<div className={TOC_WRAPPER_CLASSNAME}>
Expand All @@ -131,7 +164,7 @@ export const TableOfContents = ({
<li>
<a
onClick={() => {
toggleIsOpen()
toggleIsOpenOnMobile()
setActiveHeading("")
}}
href="#"
Expand Down Expand Up @@ -159,7 +192,7 @@ export const TableOfContents = ({
}
>
<a
onClick={toggleIsOpen}
onClick={toggleIsOpenOnMobile}
href={`#${heading.slug}`}
data-track-note="toc_link"
>
Expand Down
9 changes: 9 additions & 0 deletions site/gdocs/OwidGdoc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { DebugProvider } from "./DebugContext.js"
import { OwidGdocHeader } from "./OwidGdocHeader.js"
import StickyNav from "../blocks/StickyNav.js"
import { getShortPageCitation } from "./utils.js"
import { TableOfContents } from "../TableOfContents.js"
export const AttachmentsContext = createContext<{
linkedCharts: Record<string, LinkedChart>
linkedDocuments: Record<string, OwidGdocInterface>
Expand Down Expand Up @@ -70,6 +71,7 @@ export function OwidGdoc({
publishedAt
)
const citationText = `${shortPageCitation} Published online at OurWorldInData.org. Retrieved from: '${`${BAKED_BASE_URL}/${slug}`}' [Online Resource]`
const hasSidebarToc = content["sidebar-toc"]

const bibtex = `@article{owid-${slug.replace(/\//g, "-")},
author = {${formatAuthors({
Expand Down Expand Up @@ -119,6 +121,13 @@ export function OwidGdoc({
publishedAt={publishedAt}
breadcrumbs={breadcrumbs ?? undefined}
/>
{hasSidebarToc && content.toc ? (
<TableOfContents
headings={content.toc}
headingLevels={{ primary: 1, secondary: 2 }}
pageTitle={content.title || ""}
/>
) : null}
{content.type === "topic-page" && stickyNavLinks?.length ? (
<nav className="sticky-nav sticky-nav--dark span-cols-14 grid grid-cols-12-full-width">
<StickyNav
Expand Down
117 changes: 117 additions & 0 deletions site/gdocs/centered-article.scss
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure hover states (toggle, title vs close) are on your radar, just making a note here for good measure.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,123 @@
color: $vermillion !important;
}
}

.toc-wrapper {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this duplicates the existing site sidebar CSS because much of that CSS is stuck inside media queries (because for old-style entries, the sidebar is always open on xxlg, which we can't do here because it would obscure full-width content beneath it)

position: sticky;
top: 0;
height: 0;
// Above explorer chrome
z-index: 3;
margin-top: -48px;
.entry-sidebar {
height: 100vh;
position: absolute;
transition: margin 300ms ease;
width: 400px;
margin-left: -400px;
box-shadow: none;
@include sm-only {
width: 100vw;
margin-left: -100vw;
}
@include sm-up {
ul {
margin-left: 32px;
}
}

li {
&:first-child {
margin-top: 36px;
}

&.section {
margin-top: 20px;
}
&.subsection a {
color: $blue-60;
margin-left: 16px;
line-height: 1.125em;
}
&.active a {
border-left-color: $vermillion;
background: unset;
font-weight: bold;
}
a {
padding-left: 16px;
color: $blue-90;
border-width: 4px;
padding-right: 32px;
margin-left: 0;
font-weight: 400;

&:hover {
background: none;
text-decoration: underline;
}
}
}

.toggle-toc {
margin-left: 0;
transform: translateX(calc(100% + 16px));
position: absolute;
top: 0;
bottom: 0;
right: 0;
padding: 16px 0;
pointer-events: none;
display: unset;
transition: transform 300ms ease;
button {
@include popover-box-button;
z-index: 20;
position: sticky;
top: 16px;
pointer-events: auto;
white-space: nowrap;
box-shadow: none;
background: #fff;
border: 1px solid $blue-20;
line-height: 1.25;
padding: 6px;
border-radius: 4px;

&:hover {
background: #fff;
svg {
color: $blue-100;
}
}
svg {
margin-right: 0;
color: $blue-90;
height: 12px;
}

span {
color: $blue-90;
margin-left: 5px;
position: relative;
top: 1px;
}
}
}
&.entry-sidebar--is-open {
margin-left: 0;
.toggle-toc {
transform: translateX(-16px);
button {
border: none;
span {
display: none;
}
}
}
}
}
}
}

$banner-height: 200px;
Expand Down
2 changes: 2 additions & 0 deletions site/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const useTriggerWhenClickOutside = (
) => {
useEffect(() => {
if (!active) return
// Don't toggle if viewport width is xxlg or larger
if (window.innerWidth >= 1536) return
const handleClick = (e: MouseEvent) => {
if (container && !container.current?.contains(e.target as Node)) {
trigger(false)
Expand Down
Loading