From a3547c5a1f8923f74d3ba9e3140db646dfe8975f Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 15 May 2023 18:43:16 -0700 Subject: [PATCH] feat: Show Table of Contents in the article sidebar (#7085) --- app/javascript/packs/portal.js | 30 +------ app/javascript/portal/application.scss | 18 ++++ .../portal/components/TableOfContents.vue | 70 ++++++++++++++++ app/javascript/portal/portalHelpers.js | 84 +++++++++++++++++-- app/javascript/portal/specs/portal.spec.js | 4 +- app/views/layouts/portal.html.erb | 5 +- .../api/v1/portals/articles/show.html.erb | 13 ++- config/locales/en.yml | 1 + package.json | 8 +- tailwind.config.js | 1 - yarn.lock | 26 ++++++ 11 files changed, 204 insertions(+), 56 deletions(-) create mode 100644 app/javascript/portal/components/TableOfContents.vue diff --git a/app/javascript/packs/portal.js b/app/javascript/packs/portal.js index 5c1867658..42493615d 100644 --- a/app/javascript/packs/portal.js +++ b/app/javascript/packs/portal.js @@ -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: '', - }).$mount('#search-wrap'); - } -}; - -document.addEventListener('DOMContentLoaded', () => { - initPageSetUp(); -}); - -document.addEventListener('turbolinks:load', () => { - initPageSetUp(); -}); +document.addEventListener('turbolinks:load', InitializationHelpers.onLoad); diff --git a/app/javascript/portal/application.scss b/app/javascript/portal/application.scss index b657e2249..8a1b7461b 100644 --- a/app/javascript/portal/application.scss +++ b/app/javascript/portal/application.scss @@ -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; + } + } +} diff --git a/app/javascript/portal/components/TableOfContents.vue b/app/javascript/portal/components/TableOfContents.vue new file mode 100644 index 000000000..5e00f50aa --- /dev/null +++ b/app/javascript/portal/components/TableOfContents.vue @@ -0,0 +1,70 @@ + + diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js index 647059b73..27adad3d5 100644 --- a/app/javascript/portal/portalHelpers.js +++ b/app/javascript/portal/portalHelpers.js @@ -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 += ``; + 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: '', + }).$mount('#search-wrap'); + } + }, + + initializeTableOfContents: () => { + const isOnArticlePage = document.querySelector('#cw-hc-toc'); + if (isOnArticlePage) { + new Vue({ + components: { TableOfContents }, + data: { rows: getHeadingsfromTheArticle() }, + template: '', + }).$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(); + } + }, }; diff --git a/app/javascript/portal/specs/portal.spec.js b/app/javascript/portal/specs/portal.spec.js index 31dc06890..cd4347bad 100644 --- a/app/javascript/portal/specs/portal.spec.js +++ b/app/javascript/portal/specs/portal.spec.js @@ -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) diff --git a/app/views/layouts/portal.html.erb b/app/views/layouts/portal.html.erb index 071fde9ee..8f1dda7ae 100644 --- a/app/views/layouts/portal.html.erb +++ b/app/views/layouts/portal.html.erb @@ -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') %>' }; diff --git a/app/views/public/api/v1/portals/articles/show.html.erb b/app/views/public/api/v1/portals/articles/show.html.erb index bea1a20e8..20f14e787 100644 --- a/app/views/public/api/v1/portals/articles/show.html.erb +++ b/app/views/public/api/v1/portals/articles/show.html.erb @@ -35,9 +35,9 @@
<% if @article.author&.avatar_url&.present? %> - <%= @article.author.display_name %> + <%= @article.author.display_name %> <% end %> -
+
<%= @article.author.available_name %>

<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: @article.updated_at.strftime("%b %d, %Y")) %> @@ -46,10 +46,9 @@

-
-
-
-

<%= @parsed_content %>

-
+
+
+ <%= @parsed_content %>
+
diff --git a/config/locales/en.yml b/config/locales/en.yml index 0a9026554..2a2437566 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: diff --git a/package.json b/package.json index 9b739c030..24c61216d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tailwind.config.js b/tailwind.config.js index 1076ae315..0dc013c8e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -97,7 +97,6 @@ module.exports = { }, }, }, - variants: {}, plugins: [ // eslint-disable-next-line require('@tailwindcss/typography'), diff --git a/yarn.lock b/yarn.lock index 6cdb92044..323c690c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"