Reputation: 4883
I've a very large table
on my page. So I decided to put a horizontal scrollbar on the bottom of the table. But I would like this scrollbar to be also on top on the table.
What I have in the template is this:
<div style="overflow:auto; width:100%; height:130%">
<table id="data" style="width:100%">...</table>
</div>
Is this possible to do in HTML and CSS only?
Upvotes: 221
Views: 403357
Reputation: 25
I've been trying to find an answer for react
version but it was really difficult to find a proper solution.
Finally I got a fix from npm.
React-Double Scrollbar
https://www.npmjs.com/package/react-double-scrollbar
render() {
return (
<div>
<DoubleScrollbar>
<table>...</table>
</DoubleScrollbar>
</div>
);
}
Upvotes: 2
Reputation: 1019
Extending Dario Digregorio https://stackoverflow.com/a/63507769/4541566 answer (posted here) I was able to move the scrollbar above and below the content when the bottom of the container is scrolled into view or not.
setTimeout(()=>{
const container = document.querySelector('.container');
const content = document.querySelector('.content');
handleWindowScroll();
function handleWindowScroll() {
const viewportHeight = window.innerHeight;
const containerRect = container.getBoundingClientRect();
const containerTop = containerRect.top;
const containerHeight = containerRect.height;
const containerBottom = containerTop + containerHeight;
const scrollThreshold = 100; // adjust this value as needed
if (containerBottom <= viewportHeight + scrollThreshold) {
// bottom of container is visible in viewport
container.style.transform = '';
content.style.transform = '';
} else {
// bottom of container is not visible in viewport
container.style.transform = 'rotateX(180deg)';
content.style.transform = 'rotateX(180deg)';
}
}
window.addEventListener('scroll', handleWindowScroll);
}, 200);
Note that I am using setTimeout because of another component that I have to wait a bit, thus you may not need to use it.
Upvotes: 0
Reputation: 1322
a javascript only solution that's based on @HoldOffHunger and @bobince answers
<div id="data">
...
</div>
function doubleScroll(element) {
const scrollbar = document.createElement("div");
scrollbar.appendChild(document.createElement("div"));
scrollbar.style.overflow = "auto";
scrollbar.style.overflowY = "hidden";
scrollbar.firstChild.style.width = element.scrollWidth + "px";
scrollbar.firstChild.style.paddingTop = "1px";
scrollbar.firstChild.appendChild(document.createTextNode("\xA0"));
let running = false;
// Keep scrollbar in sync when element size changes
new ResizeObserver(() => {
scrollbar.firstChild.style.width = element.scrollWidth + "px";
}).observe(element);
scrollbar.onscroll = function () {
if (running) {
running = false;
return;
}
running = true;
element.scrollLeft = scrollbar.scrollLeft;
};
element.onscroll = function () {
if (running) {
running = false;
return;
}
running = true;
scrollbar.scrollLeft = element.scrollLeft;
};
element.parentNode.insertBefore(scrollbar, element);
}
doubleScroll(document.getElementById("data"));
Upvotes: 17
Reputation: 1352
This is my solution in vanilla js for simple usage with Bootstrap:
<div class="table-responsive table-responsive-scrollbar-top"></div>
<div class="table-responsive">
<table class="table">
<!-- ... table code ... -->
</table>
</div>
<script>
window.addEventListener('DOMContentLoaded', function() {
let mains = document.querySelectorAll('.table-responsive');
if (mains.length > 0) {
Array.from(mains).forEach(function(main) {
let top = main.previousElementSibling.matches('.table-responsive-scrollbar-top') ? main.previousElementSibling : null;
if (top) {
let timeout = false;
let toggleScrollbar;
top.style.display = 'none';
if (!top.firstElementChild) {
top.appendChild(document.createElement("div"));
}
(toggleScrollbar = function() {
if (main.offsetWidth < main.scrollWidth) {
top.style.display = 'block';
top.style.height = (main.offsetHeight - main.clientHeight) + 'px';
top.firstElementChild.style.width = main.scrollWidth + 'px';
} else {
top.style.display = 'revert';
}
})();
addEventListener('resize', (event) => {
clearTimeout(timeout);
timeout = setTimeout(toggleScrollbar, 250);
});
top.addEventListener('scroll', function(e) {
main.scrollLeft = top.scrollLeft;
});
main.addEventListener('scroll', function(e) {
top.scrollLeft = main.scrollLeft;
});
}
});
}
});
</script>
https://github.com/lsblsb/bootstrap-table-responsive-scrollbar-top
Upvotes: 1
Reputation: 462
There is one way to achieve this that I did not see anybody mentioning here.
By rotating the parent container by 180 degrees and the child-container again by 180 degrees the scrollbar will be shown at top
.parent {
transform: rotateX(180deg);
overflow-x: auto;
}
.child {
transform: rotateX(180deg);
}
For reference see the issue in the w3c repository.
Upvotes: 92
Reputation: 553
React+TypeScript rap-2-h's code ported to TypeScript.
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { ReactNode } from 'react';
import { useRef } from 'react';
const useStyles = makeStyles((theme) => ({
wrapper1: {
width: '300px',
height: '20px',
overflowX: 'scroll',
overflowY: 'hidden',
border: 'none 0px black',
},
wrapper2: {
width: '300px',
height: '100px',
overflowX: 'scroll',
overflowY: 'hidden',
border: 'none 0px black',
},
div1: {
width: '1000px',
height: '20px',
},
div2: {
width: '1000px',
height: '100px',
backgroundColor: '#88FF88',
overflow: 'auto',
}
}));
export default function TopBottomScrollBars() {
const classes = useStyles();
const wrapRef1 = useRef<HTMLDivElement>(null);
const wrapRef2 = useRef<HTMLDivElement>(null);
const handleScroll: React.EventHandler<React.UIEvent<ReactNode>> = (event: React.UIEvent<React.ReactNode> ) => {
const targetDiv: HTMLDivElement = event.target as HTMLDivElement;
if(targetDiv === wrapRef1.current && wrapRef2.current) {
wrapRef2.current.scrollLeft = targetDiv.scrollLeft;
}
else if(targetDiv === wrapRef2.current && wrapRef1.current) {
wrapRef1.current.scrollLeft = targetDiv.scrollLeft;
}
};
return (
<div>
<div ref={wrapRef1} className={classes.wrapper1} onScroll={handleScroll} >
<div id="div1" className={classes.div1}>
</div>
</div>
<div ref={wrapRef2} className={classes.wrapper2} onScroll={handleScroll}>
<div id="div2" className={classes.div2}>
aaaa bbbb cccc dddd aaaa bbbb cccc
dddd aaaa bbbb cccc dddd aaaa bbbb
cccc dddd aaaa bbbb cccc dddd aaaa
bbbb cccc dddd aaaa bbbb cccc dddd
</div>
</div>
</div>
);
}
Upvotes: 7
Reputation: 11500
I combined 2 answers here. (@simo and @bresleveloper)
https://stackblitz.com/edit/angular-double-scroll?file=src%2Fapp%2Fapp.component.html
@Component({
selector: 'app-double-scroll',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="wrapper1" #wrapper1>
<div class="div1" #div1></div>
</div>
<div class="wrapper2" #wrapper2>
<div class="div2" #div2>
<ng-content></ng-content>
</div>
</div>
`,
styles: [
`
.wrapper1, .wrapper2 { width: 100%; overflow-x: auto; overflow-y: hidden; }
`,
`
.div1 { overflow: hidden; height: 0.5px;}
`,
`
.div2 { overflow: hidden; min-width: min-content}
`
]
})
export class DoubleScrollComponent implements AfterViewInit {
@ViewChild('wrapper1') wrapper1: ElementRef<any>;
@ViewChild('wrapper2') wrapper2: ElementRef<any>;
@ViewChild('div1') div1: ElementRef<any>;
@ViewChild('div2') div2: ElementRef<any>;
constructor(private _r: Renderer2, private _cd: ChangeDetectorRef) {
}
ngAfterViewInit() {
this._cd.detach();
this._r.setStyle(this.div1.nativeElement, 'width', this.div2.nativeElement.clientWidth + 'px' );
this.wrapper1.nativeElement.onscroll = e => this.wrapper2.nativeElement.scroll((e.target as HTMLElement).scrollLeft, 0)
this.wrapper2.nativeElement.onscroll = e => this.wrapper1.nativeElement.scroll((e.target as HTMLElement).scrollLeft, 0)
}
}
<div style="width: 200px; border: 1px black dashed">
<app-double-scroll>
<div style="min-width: 400px; background-color: red; word-break: keep-all; white-space: nowrap;">
long ass text long ass text long ass text long ass text long ass text long ass text long ass text long ass text long ass text
</div>
</app-double-scroll>
<br>
<hr>
<br>
<app-double-scroll>
<div style="display: inline-block; background-color: green; word-break: keep-all; white-space: nowrap;">
short ass text
</div>
</app-double-scroll>
<br>
<hr>
<br>
<app-double-scroll>
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
<td>table cell</td>
</tr>
</tbody>
</table>
</app-double-scroll>
</div>
Upvotes: 2
Reputation: 7384
Here is an example for VueJS
index.page
<template>
<div>
<div ref="topScroll" class="top-scroll" @scroll.passive="handleScroll">
<div
:style="{
width: `${contentWidth}px`,
height: '12px'
}"
/>
</div>
<div ref="content" class="content" @scroll.passive="handleScroll">
<div
:style="{
width: `${contentWidth}px`
}"
>
Lorem ipsum dolor sit amet, ipsum dictum vulputate molestie id magna,
nunc laoreet maecenas, molestie ipsum donec lectus ut et sit, aut ut ut
viverra vivamus mollis in, integer diam purus penatibus. Augue consequat
quis phasellus non, congue tristique ac arcu cras ligula congue, elit
hendrerit lectus faucibus arcu ligula a, id hendrerit dolor nec nec
placerat. Vel ornare tincidunt tincidunt, erat amet mollis quisque, odio
cursus gravida libero aliquam duis, dolor sed nulla dignissim praesent
erat, voluptatem pede aliquam. Ut et tellus mi fermentum varius, feugiat
nullam nunc ultrices, ullamcorper pede, nunc vestibulum, scelerisque
nunc lectus integer. Nec id scelerisque vestibulum, elit sit, cursus
neque varius. Fusce in, nunc donec, volutpat mauris wisi sem, non
sapien. Pellentesque nisl, lectus eros hendrerit dui. In metus aptent
consectetuer, sociosqu massa mus fermentum mauris dis, donec erat nunc
orci.
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
contentWidth: 1000
}
},
methods: {
handleScroll(event) {
if (event.target._prevClass === 'content') {
this.$refs.topScroll.scrollLeft = this.$refs.content.scrollLeft
} else {
this.$refs.content.scrollLeft = this.$refs.topScroll.scrollLeft
}
}
}
}
</script>
<style lang="scss" scoped>
.top-scroll,
.content {
overflow: auto;
max-width: 100%;
}
.top-scroll {
margin-top: 50px;
}
</style>
TAKE NOTE on the height: 12px
.. I made it 12px so it will be noticed on Mac Users as well. But w/ windows, 0.1px is good enough
Upvotes: 2
Reputation: 3826
An AngularJs directive for achieving this: To use it, add css class double-hscroll to your element. You will need jQuery and AngularJs for this.
import angular from 'angular';
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $compile) {
$scope.name = 'Dual wielded horizontal scroller';
});
app.directive('doubleHscroll', function($compile) {
return {
restrict: 'C',
link: function(scope, elem, attr){
var elemWidth = parseInt(elem[0].clientWidth);
elem.wrap(`<div id='wrapscroll' style='width:${elemWidth}px;overflow:scroll'></div>`);
//note the top scroll contains an empty space as a 'trick'
$('#wrapscroll').before(`<div id='topscroll' style='height:20px; overflow:scroll;width:${elemWidth}px'><div style='min-width:${elemWidth}px'> </div></div>`);
$(function(){
$('#topscroll').scroll(function(){
$("#wrapscroll").scrollLeft($("#topscroll").scrollLeft());
});
$('#wrapscroll').scroll(function() {
$("#topscroll").scrollLeft($("#wrapscroll").scrollLeft());
});
});
}
};
});
Upvotes: 0
Reputation: 3504
Based on @StanleyH solution I created an AngularJS directive, demo on jsFiddle.
Easy to use:
<div data-double-scroll-bar-horizontal> {{content}} or static content </div>
For AngularJS developers
Upvotes: 5
Reputation: 6066
to all angular/nativeJs fans, implementing @simo's answer
HTML (no change)
<div class="top-scroll-wrapper">
<div class="top-scroll"></div>
</div>
CSS (no change, width: 90% is my desing)
.top-scroll-wrapper { width: 90%;height: 20px;margin: auto;padding: 0 16px;overflow-x: auto;overflow-y: hidden;}
.top-scroll { height: 20px; }
JS (like onload
) or ngAfterViewChecked
(all the as
are for TypeScript
)
let $topscroll = document.querySelector(".top-scroll") as HTMLElement
let $topscrollWrapper = document.querySelector(".top-scroll-wrapper") as HTMLElement
let $table = document.querySelectorAll('mat-card')[3] as HTMLElement
$topscroll.style.width = totalWidth + 'px'
$topscrollWrapper.onscroll = e => $table.scroll((e.target as HTMLElement).scrollLeft, 0)
$table.onscroll = e => $topscrollWrapper.scroll((e.target as HTMLElement).scrollLeft, 0)
Upvotes: 1
Reputation: 6364
Expanding on StanleyH's answer, and trying to find the minimum required, here is what I implemented:
JavaScript (called once from somewhere like $(document).ready()
):
function doubleScroll(){
$(".topScrollVisible").scroll(function(){
$(".tableWrapper")
.scrollLeft($(".topScrollVisible").scrollLeft());
});
$(".tableWrapper").scroll(function(){
$(".topScrollVisible")
.scrollLeft($(".tableWrapper").scrollLeft());
});
}
HTML (note that the widths will change the scroll bar length):
<div class="topScrollVisible" style="overflow-x:scroll">
<div class="topScrollTableLength" style="width:1520px; height:20px">
</div>
</div>
<div class="tableWrapper" style="overflow:auto; height:100%;">
<table id="myTable" style="width:1470px" class="myTableClass">
...
</table>
That's it.
Upvotes: 2
Reputation: 5843
You can use a jQuery plugin that will do the job for you :
The plugin will handle all the logic for you.
Upvotes: 11
Reputation: 485
Try using the jquery.doubleScroll
plugin :
jQuery :
$(document).ready(function(){
$('#double-scroll').doubleScroll();
});
CSS :
#double-scroll{
width: 400px;
}
HTML :
<div id="double-scroll">
<table id="very-wide-element">
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
</div>
Upvotes: 46
Reputation: 15498
First of all, great answer, @StanleyH. If someone is wondering how to make the double scroll container with dynamic width :
.wrapper1, .wrapper2 { width: 100%; overflow-x: scroll; overflow-y: hidden; }
.wrapper1 { height: 20px; }
.div1 { height: 20px; }
.div2 { overflow: none; }
$(function () {
$('.wrapper1').on('scroll', function (e) {
$('.wrapper2').scrollLeft($('.wrapper1').scrollLeft());
});
$('.wrapper2').on('scroll', function (e) {
$('.wrapper1').scrollLeft($('.wrapper2').scrollLeft());
});
});
$(window).on('load', function (e) {
$('.div1').width($('table').width());
$('.div2').width($('table').width());
});
<div class="wrapper1">
<div class="div1"></div>
</div>
<div class="wrapper2">
<div class="div2">
<table>
<tbody>
<tr>
<td>table cell</td>
<td>table cell</td>
<!-- ... -->
<td>table cell</td>
<td>table cell</td>
</tr>
</tbody>
</table>
</div>
</div>
http://jsfiddle.net/simo/67xSL/
Upvotes: 25
Reputation: 4005
Linking the scrollers worked, but in the way it's written it creates a loop which makes scrolling slow in most browsers if you click on the part of the lighter scrollbar and hold it (not when dragging the scroller).
I fixed it with a flag:
$(function() {
x = 1;
$(".wrapper1").scroll(function() {
if (x == 1) {
x = 0;
$(".wrapper2")
.scrollLeft($(".wrapper1").scrollLeft());
} else {
x = 1;
}
});
$(".wrapper2").scroll(function() {
if (x == 1) {
x = 0;
$(".wrapper1")
.scrollLeft($(".wrapper2").scrollLeft());
} else {
x = 1;
}
});
});
Upvotes: 3
Reputation: 20931
StanleyH's answer was excellent, but it had one unfortunate bug: clicking the shaded area of the scrollbar no longer jumps to the selection you click. Instead, what you get is a very small and somewhat annoying increment in the position of the scrollbar.
Tested: 4 versions of Firefox (100% affected), 4 versions of Chrome (50% affected).
Here's my jsfiddle. You can get around this with by having an on/off (true/false) var that allows only one onScroll() event to trigger at a time:
var scrolling = false;
$(".wrapper1").scroll(function(){
if(scrolling) {
scrolling = false;
return true;
}
scrolling = true;
$(".wrapper2")
.scrollLeft($(".wrapper1").scrollLeft());
});
$(".wrapper2").scroll(function(){
if(scrolling) {
scrolling = false;
return true;
}
scrolling = true;
$(".wrapper1")
.scrollLeft($(".wrapper2").scrollLeft());
});
Problem Behavior With Accepted Answer :
Actually Desired Behavior :
So, just why does this happen? If you run through the code, you'll see that wrapper1 calls wrapper2's scrollLeft, and wrapper2 calls wrapper1's scrollLeft, and repeat this infinitely, so, we have an infinite loop problem. Or, rather: the continued scrolling of the user conflicts with wrapperx's call of the scrolling, an event conflict occurs, and the end result is no jumping in the scrollbars.
Hope this helps someone else out!
Upvotes: 26
Reputation: 159
In vanilla Javascript/Angular you can do this like this:
scroll() {
let scroller = document.querySelector('.above-scroller');
let table = document.querySelector('.table');
table.scrollTo(scroller.scrollLeft,0);
}
HTML:
<div class="above-scroller" (scroll)="scroll()">
<div class="scroller"></div>
</div>
<div class="table" >
<table></table>
</div>
CSS:
.above-scroller {
overflow-x: scroll;
overflow-y:hidden;
height: 20px;
width: 1200px
}
.scroller {
width:4500px;
height: 20px;
}
.table {
width:100%;
height: 100%;
overflow: auto;
}
Upvotes: 2
Reputation: 32038
Because you might not need JQuery, here is a working Vanilla JS version based on @StanleyH answer:
var wrapper1 = document.getElementById('wrapper1');
var wrapper2 = document.getElementById('wrapper2');
wrapper1.onscroll = function() {
wrapper2.scrollLeft = wrapper1.scrollLeft;
};
wrapper2.onscroll = function() {
wrapper1.scrollLeft = wrapper2.scrollLeft;
};
#wrapper1, #wrapper2{width: 300px; border: none 0px RED;
overflow-x: scroll; overflow-y:hidden;}
#wrapper1{height: 20px; }
#wrapper2{height: 100px; }
#div1 {width:1000px; height: 20px; }
#div2 {width:1000px; height: 100px; background-color: #88FF88;
overflow: auto;}
<div id="wrapper1">
<div id="div1">
</div>
</div>
<div id="wrapper2">
<div id="div2">
aaaa bbbb cccc dddd aaaa bbbb cccc
dddd aaaa bbbb cccc dddd aaaa bbbb
cccc dddd aaaa bbbb cccc dddd aaaa
bbbb cccc dddd aaaa bbbb cccc dddd
</div>
</div>
Upvotes: 43
Reputation: 12945
If you are using iscroll.js on webkit browser or mobile browser, you could try:
$('#pageWrapper>div:last-child').css('top', "0px");
Upvotes: 0
Reputation: 2814
To simulate a second horizontal scrollbar on top of an element, put a "dummy" div
above the element that has horizontal scrolling, just high enough for a scrollbar. Then attach handlers of the "scroll" event for the dummy element and the real element, to get the other element in synch when either scrollbar is moved. The dummy element will look like a second horizontal scrollbar above the real element.
For a live example, see this fiddle
Here's the code:
HTML:
<div class="wrapper1">
<div class="div1"></div>
</div>
<div class="wrapper2">
<div class="div2">
<!-- Content Here -->
</div>
</div>
CSS:
.wrapper1, .wrapper2 {
width: 300px;
overflow-x: scroll;
overflow-y:hidden;
}
.wrapper1 {height: 20px; }
.wrapper2 {height: 200px; }
.div1 {
width:1000px;
height: 20px;
}
.div2 {
width:1000px;
height: 200px;
background-color: #88FF88;
overflow: auto;
}
JS:
$(function(){
$(".wrapper1").scroll(function(){
$(".wrapper2").scrollLeft($(".wrapper1").scrollLeft());
});
$(".wrapper2").scroll(function(){
$(".wrapper1").scrollLeft($(".wrapper2").scrollLeft());
});
});
Upvotes: 271
Reputation: 9794
As far as I'm aware this isn't possible with HTML and CSS.
Upvotes: 3