Skip to content

Commit

Permalink
Merge pull request #2869 from owid/linear-topic-page-toc
Browse files Browse the repository at this point in the history
🎉 Entry Emulator - sidebar table of contents
  • Loading branch information
ikesau authored Nov 6, 2023
2 parents a0aefaf + 3baee54 commit c439f3f
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 13 deletions.
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 @@ -196,6 +196,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}`),
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 @@ -40,8 +40,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 @@ -1431,6 +1431,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()
}
}, [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 @@ -72,6 +73,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 @@ -121,6 +123,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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,123 @@
color: $vermillion !important;
}
}

.toc-wrapper {
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

0 comments on commit c439f3f

Please sign in to comment.