mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Show Table of Contents in the article sidebar (#7085)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
70
app/javascript/portal/components/TableOfContents.vue
Normal file
70
app/javascript/portal/components/TableOfContents.vue
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -97,7 +97,6 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [
|
||||
// eslint-disable-next-line
|
||||
require('@tailwindcss/typography'),
|
||||
|
||||
26
yarn.lock
26
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"
|
||||
|
||||
Reference in New Issue
Block a user