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)

View File

@@ -44,8 +44,9 @@ By default, it renders:
searchPlaceholder: '<%= I18n.t('public_portal.search.search_placeholder') %>',
emptyPlaceholder: '<%= I18n.t('public_portal.search.empty_placeholder') %>',
loadingPlaceholder: '<%= I18n.t('public_portal.search.loading_placeholder') %>',
resultsTitle: '<%= I18n.t('public_portal.search.results_title') %>'
}
resultsTitle: '<%= I18n.t('public_portal.search.results_title') %>',
},
tocHeader: '<%= I18n.t('public_portal.toc_header') %>'
};
</script>
</html>

View File

@@ -35,9 +35,9 @@
<div class="flex flex-col items-start justify-between w-full md:flex-row md:items-center pt-2">
<div class="flex items-center space-x-2">
<% if @article.author&.avatar_url&.present? %>
<img src="<%= @article.author.avatar_url %>" alt="<%= @article.author.display_name %>" class="w-12 h-12 border rounded-full">
<img src="<%= @article.author.avatar_url %>" alt="<%= @article.author.display_name %>" class="w-12 h-12 border rounded-full pr-1">
<% end %>
<div class="pl-1">
<div>
<h5 class="text-base font-medium text-slate-900 mb-2"><%= @article.author.available_name %></h5>
<p class="text-sm font-normal text-slate-700">
<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: @article.updated_at.strftime("%b %d, %Y")) %>
@@ -46,10 +46,9 @@
</div>
</div>
</div>
<div class="max-w-6xl flex-grow w-full px-8 py-8 mx-auto space-y-12">
<article class="space-y-8">
<div class="text-slate-800 text-lg max-w-3xl prose break-words">
<p><%= @parsed_content %></p>
</div>
<div class="flex max-w-6xl w-full px-8 mx-auto">
<article id="cw-article-content" class="flex-grow flex-2 py-12 mx-auto text-slate-800 text-lg max-w-3xl prose break-words">
<%= @parsed_content %>
</article>
<div class="flex-1" id="cw-hc-toc"></div>
</div>

View File

@@ -196,6 +196,7 @@ en:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
toc_header: 'On this page'
hero:
sub_title: Search for the articles here or browse the categories below.
common:

View File

@@ -28,6 +28,7 @@
"@rails/webpacker": "5.4.4",
"@sentry/tracing": "^6.19.7",
"@sentry/vue": "^6.19.7",
"@sindresorhus/slugify": "1.1.0",
"@tailwindcss/typography": "0.2.0",
"activestorage": "^5.2.6",
"axios": "^0.21.2",
@@ -133,13 +134,6 @@
"pre-push": "sh bin/validate_push"
}
},
"jest": {
"collectCoverage": true,
"coverageReporters": [
"lcov",
"text"
]
},
"lint-staged": {
"app/**/*.{js,vue}": [
"eslint --fix",

View File

@@ -97,7 +97,6 @@ module.exports = {
},
},
},
variants: {},
plugins: [
// eslint-disable-next-line
require('@tailwindcss/typography'),

View File

@@ -2887,6 +2887,22 @@
"@sentry/utils" "6.19.7"
tslib "^1.9.3"
"@sindresorhus/slugify@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/slugify/-/slugify-1.1.0.tgz#2f195365d9b953384305b62664b44b4036c49430"
integrity sha512-ujZRbmmizX26yS/HnB3P9QNlNa4+UvHh+rIse3RbOXLp8yl6n1TxB4t7NHggtVgS8QmmOtzXo48kCxZGACpkPw==
dependencies:
"@sindresorhus/transliterate" "^0.1.1"
escape-string-regexp "^4.0.0"
"@sindresorhus/transliterate@^0.1.1":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz#ffce368271d153550e87de81486004f2637425af"
integrity sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==
dependencies:
escape-string-regexp "^2.0.0"
lodash.deburr "^4.1.0"
"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
@@ -8468,6 +8484,11 @@ escape-string-regexp@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
escodegen@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
@@ -12048,6 +12069,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.deburr@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
integrity sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==
lodash.get@^4.0:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"