Skip to content

Commit

Permalink
Horizontal table of contents implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Mistry authored and Mistry committed Mar 25, 2024
1 parent 209174b commit b9d1aef
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 7 deletions.
191 changes: 191 additions & 0 deletions src/components/story/horizontal-menu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<template>
<div class="navbar sticky">
<ul>
<li v-if="introExists">
<a
class="items-center px-2 py-1 mx-1 cursor-pointer"
@click="scrollToChapter('intro', 0, activeChapterIndex, false)"
v-tippy="{
delay: '200',
placement: 'right',
content: $t('chapters.return'),
animateFill: true,
animation: 'chapter-menu'
}"
v-if="plugin"
>
<span class="flex-1 ml-4 overflow-hidden leading-normal overflow-ellipsis whitespace-nowrap">{{
$t('chapters.return')
}}</span>
</a>

<router-link
:to="{ hash: '#intro' }"
class="flex items-center px-2 py-1 mx-1"
target
v-tippy="{
delay: '200',
placement: 'right',
content: $t('chapters.return'),
animateFill: true,
animation: 'chapter-menu'
}"
v-else
>
<span class="flex-1 overflow-hidden leading-normal overflow-ellipsis whitespace-nowrap">{{
$t('chapters.return')
}}</span>
</router-link>
</li>
<li v-for="(slide, idx) in slides" :key="idx" :class="{ 'is-active': activeChapterIndex === idx }">
<!-- using router-link causes a page refresh which breaks plugin -->
<a
class="flex items-center px-2 py-1 mx-1 cursor-pointer"
@click="
scrollToChapter(
`${idx}-${slide.title.toLowerCase().replaceAll(' ', '-')}`,
idx,
activeChapterIndex,
false
)
"
v-tippy="{
delay: '200',
placement: 'right',
content: slide.title,
animateFill: true,
animation: 'chapter-menu'
}"
v-if="plugin"
>
<span class="flex-1 ml-4 overflow-hidden leading-normal overflow-ellipsis whitespace-nowrap">{{
slide.title
}}</span>
</a>

<router-link
:to="{ hash: `#${idx}-${slide.title.toLowerCase().replaceAll(' ', '-')}` }"
@click="
scrollToChapter(
`${idx}-${slide.title.toLowerCase().replaceAll(' ', '-')}`,
idx,
activeChapterIndex,
true
)
"
class="flex items-center px-2 py-1 mx-1"
target
v-tippy="{
delay: '200',
placement: 'right',
content: slide.title,
animateFill: true,
animation: 'chapter-menu'
}"
v-else
>
<span class="flex-1 overflow-hidden leading-normal overflow-ellipsis whitespace-nowrap">{{
slide.title
}}</span>
</router-link>
</li>
</ul>
</div>
</template>

<script setup lang="ts">
import type { PropType } from 'vue';
import { ref, onMounted } from 'vue';
import { Slide } from '@storylines/definitions';
defineProps({
slides: {
type: Array as PropType<Array<Slide>>,
required: true
},
activeChapterIndex: {
type: Number,
required: true
},
lang: {
type: String,
required: true
},
plugin: {
type: Boolean
}
});
const introExists = ref(false);
onMounted(() => {
const introSection = document.getElementById('intro');
introExists.value = !!introSection;
});
const scrollToChapter = (id: string, idx: number, last_idx: number, router_link: boolean): void => {
const el = document.getElementById(id);
const toc = document.querySelector('.navbar');
if (el && toc) {
const topOffset = toc.clientHeight;
if (!router_link) {
el.scrollIntoView({ behavior: 'smooth' });
}
const timeout = Math.ceil(Math.abs(idx - last_idx) / 5) * 600;
setTimeout(() => {
window.scrollBy({ left: 0, top: -topOffset, behavior: 'smooth' });
}, timeout);
}
};
</script>

<style lang="scss" scoped>
.navbar {
background-color: #fff;
border-bottom: 2px;
border-color: rgba(229, 231, 235, var(--tw-border-opacity));
position: sticky;
height: 100%;
width: 100%;
margin: 0;
display: flex;
justify-content: center;
}
.navbar ul {
display: flex;
list-style-type: none;
text-align: center;
justify-content: center;
flex-wrap: wrap;
overflow: hidden;
width: 100%;
padding: 5px;
margin: auto;
}
.navbar ul li {
float: left;
width: 12%;
border-radius: 20px;
a {
text-overflow: ellipsis;
}
a:hover {
text-decoration: none;
color: inherit;
}
a:focus {
text-decoration: none;
color: inherit;
}
a:visited {
color: inherit;
}
&.is-active {
background-color: var(--sr-accent-colour);
font-weight: bold;
}
}
</style>
1 change: 0 additions & 1 deletion src/components/story/introduction.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ onMounted(() => {
// obtain logo from ZIP file if it exists
if (props.configFileStructure) {
const logo = props.config.logo?.src;
if (logo) {
const logoSrc = `${logo.substring(logo.indexOf('/') + 1)}`;
const logoFile = props.configFileStructure.zip.file(logoSrc);
Expand Down
29 changes: 25 additions & 4 deletions src/components/story/story-content.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
<template>
<div class="flex items-stretch">
<chapter-menu
class="side-menu"
<div class="items-stretch">
<!-- :class="{ flex: !$props.config?.tocOrientation || $props.config?.tocOrientation === 'vertical' }" -->
<!-- v-if="$props.config?.tocOrientation === 'horizontal'" -->
<horizontal-menu
class="top-menu"
:active-chapter-index="activeChapterIndex"
:slides="config.slides"
:plugin="!!configFileStructure || !!plugin"
:lang="lang"
:style="{ top: headerHeight + 'px' }"
/>
<!-- <chapter-menu
class="side-menu"
:active-chapter-index="activeChapterIndex"
:slides="config.slides"
:plugin="!!configFileStructure || !!plugin"
:lang="lang"
v-else
/> -->

<VueScrollama class="relative story-scrollama w-full flex-grow min-w-0" @step-enter="stepEnter">
<div
Expand All @@ -32,6 +43,7 @@ import VueScrollama from 'vue3-scrollama';
import { ConfigFileStructure, StoryRampConfig } from '@storylines/definitions';
import ChapterMenu from './chapter-menu.vue';
import HorizontalMenu from './horizontal-menu.vue';
import Slide from './slide.vue';
const route = useRoute();
Expand All @@ -51,6 +63,9 @@ defineProps({
},
plugin: {
type: Boolean
},
headerHeight: {
type: Number
}
});
Expand Down Expand Up @@ -118,10 +133,16 @@ const stepEnter = ({ element }: { element: HTMLElement }): void => {
box-shadow: 0 3px 6px 0px rgba(0, 0, 0, 0.1), 0 2px 4px 0px rgba(0, 0, 0, 0.06);
}
}
.top-menu {
z-index: 50;
width: 100%;
}
@media screen and (max-width: 640px) {
.side-menu {
display: none;
}
.top-menu {
display: none;
}
}
</style>
13 changes: 11 additions & 2 deletions src/components/story/story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@

<div v-else-if="loadStatus === 'loaded'">
<div class="storyramp-app bg-white" v-if="config !== undefined">
<header class="story-header sticky top-0 w-full h-16 leading-9 bg-white border-b border-gray-200">
<header
id="story-header"
class="story-header sticky top-0 w-full h-16 leading-9 bg-white border-b border-gray-200"
>
<div class="flex w-full sm:px-6 py-3 mx-auto">
<mobile-menu
class="mobile-menu"
Expand All @@ -37,7 +40,7 @@
<intro :config="config.introSlide"></intro>

<div class="w-full mx-auto pb-10" id="story">
<story-content :config="config" :lang="lang" @step="updateActiveIndex" />
<story-content :config="config" :lang="lang" :headerHeight="headerHeight" @step="updateActiveIndex" />
</div>

<footer class="p-8 pt-2 text-right text-sm">
Expand Down Expand Up @@ -75,6 +78,7 @@ const route = useRoute();
const config = ref<StoryRampConfig | undefined>(undefined);
const loadStatus = ref('loading');
const activeChapterIndex = ref(-1);
const headerHeight = ref(0);
const lang = ref('en');
onMounted(() => {
Expand Down Expand Up @@ -146,6 +150,11 @@ const fetchConfig = (uid: string, lang: string): void => {
const updateActiveIndex = (idx: number): void => {
activeChapterIndex.value = idx;
// determine header height
const headerH = document.getElementById('story-header');
if (headerH) {
headerHeight.value = headerH.clientHeight;
}
};
</script>

Expand Down
1 change: 1 addition & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface StoryRampConfig {
slides: Slide[];
contextLink: string;
contextLabel: string;
tocOrientation: string;
dateModified: string;
}

Expand Down

0 comments on commit b9d1aef

Please sign in to comment.