Skip to content

Commit d279b08

Browse files
authored
Merge pull request #5685 from BookStackApp/sidebar_rejig
Tri-layout sidebar enhancements
2 parents 181ab91 + 0dcb2ec commit d279b08

File tree

5 files changed

+101
-26
lines changed

5 files changed

+101
-26
lines changed

resources/js/components/dropdown.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {onSelect} from '../services/dom.ts';
1+
import {findClosestScrollContainer, onSelect} from '../services/dom.ts';
22
import {KeyboardNavigationHandler} from '../services/keyboard-navigation.ts';
33
import {Component} from './component';
44

@@ -33,7 +33,8 @@ export class Dropdown extends Component {
3333
const menuOriginalRect = this.menu.getBoundingClientRect();
3434
let heightOffset = 0;
3535
const toggleHeight = this.toggle.getBoundingClientRect().height;
36-
const dropUpwards = menuOriginalRect.bottom > window.innerHeight;
36+
const containerBounds = findClosestScrollContainer(this.menu).getBoundingClientRect();
37+
const dropUpwards = menuOriginalRect.bottom > containerBounds.bottom;
3738
const containerRect = this.container.getBoundingClientRect();
3839

3940
// If enabled, Move to body to prevent being trapped within scrollable sections

resources/js/components/tri-layout.js renamed to resources/js/components/tri-layout.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import {Component} from './component';
22

33
export class TriLayout extends Component {
4-
5-
setup() {
4+
private container!: HTMLElement;
5+
private tabs!: HTMLElement[];
6+
private sidebarScrollContainers!: HTMLElement[];
7+
8+
private lastLayoutType = 'none';
9+
private onDestroy: (()=>void)|null = null;
10+
private scrollCache: Record<string, number> = {
11+
content: 0,
12+
info: 0,
13+
};
14+
private lastTabShown = 'content';
15+
16+
setup(): void {
617
this.container = this.$refs.container;
718
this.tabs = this.$manyRefs.tab;
8-
9-
this.lastLayoutType = 'none';
10-
this.onDestroy = null;
11-
this.scrollCache = {
12-
content: 0,
13-
info: 0,
14-
};
15-
this.lastTabShown = 'content';
19+
this.sidebarScrollContainers = this.$manyRefs.sidebarScrollContainer;
1620

1721
// Bind any listeners
1822
this.mobileTabClick = this.mobileTabClick.bind(this);
@@ -22,9 +26,11 @@ export class TriLayout extends Component {
2226
window.addEventListener('resize', () => {
2327
this.updateLayout();
2428
}, {passive: true});
29+
30+
this.setupSidebarScrollHandlers();
2531
}
2632

27-
updateLayout() {
33+
updateLayout(): void {
2834
let newLayout = 'tablet';
2935
if (window.innerWidth <= 1000) newLayout = 'mobile';
3036
if (window.innerWidth > 1400) newLayout = 'desktop';
@@ -56,33 +62,30 @@ export class TriLayout extends Component {
5662
};
5763
}
5864

59-
setupDesktop() {
65+
setupDesktop(): void {
6066
//
6167
}
6268

6369
/**
6470
* Action to run when the mobile info toggle bar is clicked/tapped
65-
* @param event
6671
*/
67-
mobileTabClick(event) {
68-
const {tab} = event.target.dataset;
72+
mobileTabClick(event: MouseEvent): void {
73+
const tab = (event.target as HTMLElement).dataset.tab || '';
6974
this.showTab(tab);
7075
}
7176

7277
/**
7378
* Show the content tab.
7479
* Used by the page-display component.
7580
*/
76-
showContent() {
81+
showContent(): void {
7782
this.showTab('content', false);
7883
}
7984

8085
/**
8186
* Show the given tab
82-
* @param {String} tabName
83-
* @param {Boolean }scroll
8487
*/
85-
showTab(tabName, scroll = true) {
88+
showTab(tabName: string, scroll: boolean = true): void {
8689
this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
8790

8891
// Set tab status
@@ -97,7 +100,7 @@ export class TriLayout extends Component {
97100

98101
// Set the scroll position from cache
99102
if (scroll) {
100-
const pageHeader = document.querySelector('header');
103+
const pageHeader = document.querySelector('header') as HTMLElement;
101104
const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;
102105
document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;
103106
setTimeout(() => {
@@ -108,4 +111,30 @@ export class TriLayout extends Component {
108111
this.lastTabShown = tabName;
109112
}
110113

114+
setupSidebarScrollHandlers(): void {
115+
for (const sidebar of this.sidebarScrollContainers) {
116+
sidebar.addEventListener('scroll', () => this.handleSidebarScroll(sidebar), {
117+
passive: true,
118+
});
119+
this.handleSidebarScroll(sidebar);
120+
}
121+
122+
window.addEventListener('resize', () => {
123+
for (const sidebar of this.sidebarScrollContainers) {
124+
this.handleSidebarScroll(sidebar);
125+
}
126+
});
127+
}
128+
129+
handleSidebarScroll(sidebar: HTMLElement): void {
130+
const scrollable = sidebar.clientHeight !== sidebar.scrollHeight;
131+
const atTop = sidebar.scrollTop === 0;
132+
const atBottom = (sidebar.scrollTop + sidebar.clientHeight) === sidebar.scrollHeight;
133+
134+
if (sidebar.parentElement) {
135+
sidebar.parentElement.classList.toggle('scroll-away-from-top', !atTop && scrollable);
136+
sidebar.parentElement.classList.toggle('scroll-away-from-bottom', !atBottom && scrollable);
137+
}
138+
}
139+
111140
}

resources/js/services/dom.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,22 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number)
256256
export function hashElement(element: HTMLElement): string {
257257
const normalisedElemText = (element.textContent || '').replace(/\s{2,}/g, '');
258258
return cyrb53(normalisedElemText);
259+
}
260+
261+
/**
262+
* Find the closest scroll container parent for the given element
263+
* otherwise will default to the body element.
264+
*/
265+
export function findClosestScrollContainer(start: HTMLElement): HTMLElement {
266+
let el: HTMLElement|null = start;
267+
do {
268+
const computed = window.getComputedStyle(el);
269+
if (computed.overflowY === 'scroll') {
270+
return el;
271+
}
272+
273+
el = el.parentElement;
274+
} while (el);
275+
276+
return document.body;
259277
}

resources/sass/_layout.scss

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,10 +389,12 @@ body.flexbox {
389389
.tri-layout-right {
390390
grid-area: c;
391391
min-width: 0;
392+
position: relative;
392393
}
393394
.tri-layout-left {
394395
grid-area: a;
395396
min-width: 0;
397+
position: relative;
396398
}
397399

398400
@include mixins.larger-than(vars.$bp-xxl) {
@@ -431,7 +433,8 @@ body.flexbox {
431433
grid-template-areas: "a b b";
432434
grid-template-columns: 1fr 3fr;
433435
grid-template-rows: min-content min-content 1fr;
434-
padding-inline-end: vars.$l;
436+
margin-inline-start: (vars.$m + vars.$xxs);
437+
margin-inline-end: (vars.$m + vars.$xxs);
435438
}
436439
.tri-layout-sides {
437440
grid-column-start: a;
@@ -452,6 +455,8 @@ body.flexbox {
452455
height: 100%;
453456
scrollbar-width: none;
454457
-ms-overflow-style: none;
458+
padding-inline: vars.$m;
459+
margin-inline: -(vars.$m);
455460
&::-webkit-scrollbar {
456461
display: none;
457462
}
@@ -520,4 +525,26 @@ body.flexbox {
520525
margin-inline-start: 0;
521526
margin-inline-end: 0;
522527
}
528+
}
529+
530+
/**
531+
* Scroll Indicators
532+
*/
533+
.scroll-away-from-top:before,
534+
.scroll-away-from-bottom:after {
535+
content: '';
536+
display: block;
537+
position: absolute;
538+
@include mixins.lightDark(color, #F2F2F2, #111);
539+
left: 0;
540+
top: 0;
541+
width: 100%;
542+
height: 50px;
543+
background: linear-gradient(to bottom, currentColor, transparent);
544+
z-index: 2;
545+
}
546+
.scroll-away-from-bottom:after {
547+
top: auto;
548+
bottom: 0;
549+
background: linear-gradient(to top, currentColor, transparent);
523550
}

resources/views/layouts/tri.blade.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ class="tri-layout-mobile-tab px-m py-m text-link active">
2828
<div refs="tri-layout@container" class="tri-layout-container" @yield('container-attrs') >
2929

3030
<div class="tri-layout-sides print-hidden">
31-
<div class="tri-layout-sides-content">
31+
<div refs="tri-layout@sidebar-scroll-container" class="tri-layout-sides-content">
3232
<div class="tri-layout-right print-hidden">
33-
<aside class="tri-layout-right-contents">
33+
<aside refs="tri-layout@sidebar-scroll-container" class="tri-layout-right-contents">
3434
@yield('right')
3535
</aside>
3636
</div>
3737

3838
<div class="tri-layout-left print-hidden" id="sidebar">
39-
<aside class="tri-layout-left-contents">
39+
<aside refs="tri-layout@sidebar-scroll-container" class="tri-layout-left-contents">
4040
@yield('left')
4141
</aside>
4242
</div>

0 commit comments

Comments
 (0)