feat: Show Table of Contents in the article sidebar (#7085)

This commit is contained in:
Pranav Raj S
2023-05-15 18:43:16 -07:00
committed by GitHub
parent 0f776a173c
commit a3547c5a1f
11 changed files with 204 additions and 56 deletions

View File

@@ -1,35 +1,9 @@
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so that it will be compiled.
import Vue from 'vue';
import Rails from '@rails/ujs';
import Turbolinks from 'turbolinks';
import PublicArticleSearch from '../portal/components/PublicArticleSearch.vue';
import { navigateToLocalePage } from '../portal/portalHelpers';
import '../portal/application.scss';
import { InitializationHelpers } from '../portal/portalHelpers';
Rails.start();
Turbolinks.start();
const initPageSetUp = () => {
navigateToLocalePage();
const isSearchContainerAvailable = document.querySelector('#search-wrap');
if (isSearchContainerAvailable) {
new Vue({
components: { PublicArticleSearch },
template: '<PublicArticleSearch />',
}).$mount('#search-wrap');
}
};
document.addEventListener('DOMContentLoaded', () => {
initPageSetUp();
});
document.addEventListener('turbolinks:load', () => {
initPageSetUp();
});
document.addEventListener('turbolinks:load', InitializationHelpers.onLoad);

View File

@@ -16,3 +16,21 @@ body {
-webkit-font-smoothing: antialiased;
height: 100%;
}
// Taking these utils from tailwind 3.x.x, need to remove once we upgrade
.scroll-mt-24 {
scroll-margin-top: 6rem;
}
.top-24 {
top: 6rem;
}
.heading {
&:hover {
a {
visibility: visible;
}
}
}

View File

@@ -0,0 +1,70 @@
<template>
<div class="hidden lg:block flex-1 scroll-mt-24 pl-4">
<div v-if="rows.length > 0" class="sticky top-24 py-12 overflow-auto">
<nav class="max-w-2xl">
<h2
id="on-this-page-title"
class="text-slate-800 font-semibold tracking-wide border-b mb-3 leading-7"
>
{{ tocHeader }}
</h2>
<ol role="list" class="mt-4 space-y-3 text-base">
<li v-for="element in rows" :key="element.slug" class="leading-6">
<p :class="getClassName(element)">
<a
:href="`#${element.slug}`"
data-turbolinks="false"
class="text-base text-slate-800 cursor-pointer"
>
{{ element.title }}
</a>
</p>
</li>
</ol>
</nav>
</div>
</div>
</template>
<script>
export default {
props: {
rows: {
type: Array,
default: () => [],
},
},
computed: {
tocHeader() {
return window.portalConfig.tocHeader;
},
h1Count() {
return this.rows.filter(el => el.tag === 'h1').length;
},
h2Count() {
return this.rows.filter(el => el.tag === 'h2').length;
},
},
methods: {
getClassName(el) {
if (el.tag === 'h1') {
return '';
}
if (el.tag === 'h2') {
if (this.h1Count > 0) {
return 'ml-2';
}
return '';
}
if (el.tag === 'h3') {
if (!this.h1Count && !this.h2Count) {
return '';
}
return 'ml-8';
}
return '';
},
},
};
</script>

View File

@@ -1,13 +1,79 @@
export const navigateToLocalePage = () => {
const allLocaleSwitcher = document.querySelector('.locale-switcher');
import slugifyWithCounter from '@sindresorhus/slugify';
import Vue from 'vue';
if (!allLocaleSwitcher) {
return false;
}
import PublicArticleSearch from './components/PublicArticleSearch.vue';
import TableOfContents from './components/TableOfContents.vue';
const { portalSlug } = allLocaleSwitcher.dataset;
allLocaleSwitcher.addEventListener('change', event => {
window.location = `/hc/${portalSlug}/${event.target.value}/`;
export const getHeadingsfromTheArticle = () => {
const rows = [];
const articleElement = document.getElementById('cw-article-content');
articleElement.querySelectorAll('h1, h2, h3').forEach(element => {
const slug = slugifyWithCounter(element.innerText);
element.id = slug;
element.className = 'scroll-mt-24 heading';
element.innerHTML += `<a class="invisible text-slate-600 ml-3" href="#${slug}" title="${element.innerText}" data-turbolinks="false">#</a>`;
rows.push({
slug,
title: element.innerText,
tag: element.tagName.toLowerCase(),
});
});
return false;
return rows;
};
export const InitializationHelpers = {
navigateToLocalePage: () => {
const allLocaleSwitcher = document.querySelector('.locale-switcher');
if (!allLocaleSwitcher) {
return false;
}
const { portalSlug } = allLocaleSwitcher.dataset;
allLocaleSwitcher.addEventListener('change', event => {
window.location = `/hc/${portalSlug}/${event.target.value}/`;
});
return false;
},
initalizeSearch: () => {
const isSearchContainerAvailable = document.querySelector('#search-wrap');
if (isSearchContainerAvailable) {
new Vue({
components: { PublicArticleSearch },
template: '<PublicArticleSearch />',
}).$mount('#search-wrap');
}
},
initializeTableOfContents: () => {
const isOnArticlePage = document.querySelector('#cw-hc-toc');
if (isOnArticlePage) {
new Vue({
components: { TableOfContents },
data: { rows: getHeadingsfromTheArticle() },
template: '<table-of-contents :rows="rows" />',
}).$mount('#cw-hc-toc');
}
},
initialize: () => {
InitializationHelpers.navigateToLocalePage();
InitializationHelpers.initalizeSearch();
InitializationHelpers.initializeTableOfContents();
},
onLoad: () => {
InitializationHelpers.initialize();
if (window.location.hash) {
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual';
}
const a = document.createElement('a');
a.href = window.location.hash;
a['data-turbolinks'] = false;
a.click();
}
},
};

View File

@@ -1,4 +1,4 @@
import { navigateToLocalePage } from '../portalHelpers';
import { InitializationHelpers } from '../portalHelpers';
describe('#navigateToLocalePage', () => {
it('returns correct cookie name', () => {
@@ -14,7 +14,7 @@ describe('#navigateToLocalePage', () => {
callback({ target: { value: 1 } });
});
navigateToLocalePage();
InitializationHelpers.navigateToLocalePage();
expect(allLocaleSwitcher.addEventListener).toBeCalledWith(
'change',
expect.any(Function)