需求

这个是 Google Ads 里的 table。

那个 horizontal scrollbar 可以 sticky bottom。

我们知道 scrollbar 是游览器原生的,我们能做的 styling 少之又少,挺多只能调 size, color 而已。要让它 sticky bottom 根本不可能。

实现思路

首先要弄一个假的 scrollbar 出来。

怎么理解,怎么弄?

做一个 div1 > div2

div1 设置 max-width, overflow auto

div2 设置 width。

这样一个 horizontal scrollbar 就出来的。

这个 div 里只有一个 scrollbar 没有其它内容,所以它看上去就是一个 scrollbar。

而 div 是可以 sticky bottom 的 (当然上面这个例子,使用的不是 CSS 原生的 sticky 功能,而是模拟的)。

这样我们就有了一个可以 sticky bottom 的 scrollbar。

接着我们把原生的 horizontal scrollbar hide 起来,这样看上去就 ok 了。

看是没有问题了,但是交互还需要搞一搞。

监听假 scrollbar 同步 scrollLeft 给 container,反过来也需要,监听 container scroll 同步 scrollLeft 给假 scrollbar。

这样就大功告成了。

破坏性

但凡 “假的 / 模拟的” 都是旁门左道,一定会引起一些 bug 之类的。所以一定要控制好范围,避免失控。

下图是我们常见的盒子 container

offsetHeight 的计算是 border to border (border + scrollbar + padding + content)

clientHeight 的计算是 padding to padding (padding + content, 没有 border 和 scrollbar)

问题来了,我们的假 scrollbar 要放在 container 里面还是外面?

如果放在里面,那么它会在 padding 之上 (因为它算是内容丫),而不是取代 native scrollbar 的位置 (padding 之下)。

于是我们需要 remove container 的 padding-bottom 然后在假 scrollbar 补回 padding-bottom。

即便看上去没问题,但是 clientHeight 的计算肯定就错了,因为假 scrollbar 被当成了内容,而 clientHeight 是不应该计算 scrollbar height 的。

把假 scrollbar 放到 container 外面也有类似的问题,我们需要 remove container 的 border-bottom,然后在假 scrollbar 补回 border-bottom。

这回 clientHeight 对了,但是 offsetHeight 计算却错了,少算了一个 border-bottom。

所以不管放哪一边总会影响到某些地方,这就是旁门左道的代价。

下面例子我选择放外面,因为放里面还需要用上 sticky left 会更麻烦。

Step by Step

搭环境

index.html

<div class="vertical-container">
<div class="horizontal-container">
<div class="my-content"></div>
</div>
</div>

index.scss

.vertical-container {
width: max-content;
margin-inline: auto;
max-height: 512px;
overflow-y: auto;
} .horizontal-container {
max-width: 768px;
overflow: auto; &.hide-scrollbar {
&::-webkit-scrollbar {
height: 0;
}
scrollbar-width: none;
}
} .my-content {
width: 500px;
height: 10px;
background-color: pink;
}

注意那个 class hide-scrollbar

由于 JS 无法 querySelector 伪元素,所以 hide scrollbar 只能让 CSS 负责了。

index.ts

首先 query container

const container = document.querySelector<HTMLElement>('.horizontal-container')!;

然后做一些 first time setup

// 创捷 scrollbar
const scrollbar = document.createElement('div');
// 创建 scrollbar content
const scrollbarContent = document.createElement('div');
// 把 scrollbar content 插入到 scrollbar
scrollbar.appendChild(scrollbarContent);
// 把 scrollbar 插入到 container next sibling
container.parentElement!.insertBefore(scrollbar, container.nextElementSibling); // 设置 scrollbar 一些 style
scrollbar.style.overflowX = 'auto';
scrollbar.style.overflowY = 'hidden'; // 监听 scroll 同步 container 和 scrollbar 的 scrollLeft
scrollbar.addEventListener('scroll', () => {
container.scrollLeft = scrollbar.scrollLeft;
});
container.addEventListener('scroll', () => {
scrollbar.scrollLeft = container.scrollLeft;
});

因为我们需要监听 container resize,所以特别区分 first time setup。

接着,封装一个 getContainerInfo 函数

// container info 接口
interface ContainerInfo {
clientWidth: number;
scrollWidth: number;
hasScrollbar: boolean;
scrollbarHeight: number;
} // 记入最后一次的 scrollbar height
let lastScrollbarHeight = 0;
function getContainerInfo(container: HTMLElement): ContainerInfo {
// getElementSize 是一个方便拿 element size 的功能,把它当作是 getComputedStyle 就可以了
const containerSize = getElementSize(container);
const containerClientWidth = containerSize.client.width;
const containerScrollWidth = containerSize.scroll.size.width;
// 判断有没有 scrollbar 出现
const hasScrollbar = containerClientWidth !== containerScrollWidth;
// 计算 native scrollbar 的 height
let scrollbarHeight = containerSize.offset.height - containerSize.border.block - containerSize.client.height; // 因为我们会 hide native scrollbar,
// 所以第一次可以拿到 scrollbar height 但是第二次可能就拿不到了
// 所以我们需要把 scrollbar height 存起来
if (hasScrollbar && scrollbarHeight !== 0) {
lastScrollbarHeight = scrollbarHeight;
} // 第二次拿不到 scrollbar height 的时候,我们拿存起来的来用
if (hasScrollbar && scrollbarHeight === 0) {
scrollbarHeight = lastScrollbarHeight;
// Firefox 是永远拿不到 scrollbar height 的,给它一个默认 12 就好。
if (scrollbarHeight === 0) scrollbarHeight = 12;
} return {
clientWidth: containerClientWidth,
scrollWidth: containerScrollWidth,
hasScrollbar,
scrollbarHeight,
};
}

在 first setup 之前 getContainerInfo

必须提前读取 container information,如果在 first setup 后才读取会导致游览器立刻 repaint / reflow。

接着,封装一个 updateSize 函数

function updateSize(
container: HTMLElement,
scrollbar: HTMLElement,
scrollbarContent: HTMLElement,
containerInfo: ContainerInfo,
) {
const { clientWidth, scrollWidth, hasScrollbar, scrollbarHeight } = containerInfo;
// 如果需求 scrollbar 那就 hide native scrollbar
container.classList[hasScrollbar ? 'add' : 'remove']('hide-scrollbar');
// 如果不需要 scrollbar 就 display none 假 scrollbar
if (!hasScrollbar) scrollbar.style.display = 'none'; if (hasScrollbar) {
// 如果需要 scrollbar 就 update scrollbar 和 scrollbar content 的 size
scrollbar.style.removeProperty('display');
scrollbar.style.maxWidth = `${clientWidth}px`;
scrollbar.style.maxHeight = `${scrollbarHeight}px`;
scrollbarContent.style.height = `${scrollbarHeight}px`;
scrollbarContent.style.width = `${scrollWidth}px`;
}
}

在 first setup 之后调用 updateSize for firstload

做一个 resize 监听

// StgResizeObserver 是一个基于 RxJS 的 ResizeObserver
// 把它当作 native 的 ResizeObserver 看待就可以了
const ro = new StgResizeObserver();
// 注意监听的是 container 的所有 child elements
// 因为 container 已经 overflow 了,它是不会 resize 的, resize 的是它的 children
merge(...Array.from(container.children).map(el => ro.observe(el))).subscribe(() => {
// 每当 resize 就重新 getContainerInfo + updateSize
const containerInfo = getContainerInfo(container);
updateSize(container, scrollbar, scrollbarContent, containerInfo);
});

这样就大功告成了。

提醒:container 不支持放 border 哦,因为我选择的是把假 scrollbar 放到 container 之外,如果是放在 container 里面的话就支持 border 但不支持 padding,同时需要 sticky left 比较麻烦。有兴趣的可以自己玩一玩。

至于如何让假 scrollbar sticky bottom,请参考:CSS & JS Effect – Simulation Position Sticky (用 JavaScript 实现 position sticky)

CSS & JS Effect – sticky horizontal scrollbar的更多相关文章

  1. 前端工程师面试问题归纳(一、问答类html/css/js基础)

    一.参考资源 1.前端面试题及答案整理(一) 2.2017年前端面试题整理汇总100题 3.2018最新Web前端经典面试试题及答案 4.[javascript常见面试题]常见前端面试题及答案 5.W ...

  2. CSS & JS 制作滚动幻灯片

    ==================纯CSS方式==================== <!DOCTYPE html> <html> <head> <met ...

  3. 【转】Maven Jetty 插件的问题(css/js等目录死锁)的解决

    Maven Jetty 插件的问题(css/js等目录死锁,不能自动刷新)的解决:   1. 打开下面的目录:C:\Users\用户名\.m2\repository\org\eclipse\jetty ...

  4. Css Js Loader For Zencart

    Css Js Loader 描述:这个插件很早就出来了,可能知道人非常少 这个插件的功能是整合所有的网站的CSS和JS内容到一个文件里边. 因为CSS和JS文件到了一个文件,加快了程序的运行 在配合其 ...

  5. 购物车数字加减按钮HTML+CSS+JS(有需要嫌麻烦的小伙伴拿走不谢)

    之前在写详情页的时候,如下图 因为自己嫌麻烦,就去看其他网站是怎么写的,想直接拿来用,后来看来看去觉得写得很麻烦,于是最后还是决定自己写,附上HTML+CSS+JS代码,一条龙一站式贴心服务2333 ...

  6. vs合并压缩css,js插件——Bundler & Minifier

    之前做了一个大转盘的抽奖活动,因为比较火,部分用户反馈看不到页面的情况,我怀疑js加载请求过慢导致,所以今天针对之前的一个页面进行调试优化. 首先想到的是对页面的js和css进行压缩优化,百度了下vs ...

  7. nginx资源定向 css js路径问题

    今天玩玩项目,学学nginx发现还不错,速度还可以,但是CSS JS确无法使用,原来Iginx配置时需要对不同类型的文件配置规则,真是很郁闷,不过想想也还是很有道理.闲暇之际,把配置贴上来.#user ...

  8. IIS7的集成模式下如何让自定义的HttpModule不处理静态文件(.html .css .js .jpeg等)请求

    今天将开发好的ASP.NET站点部署到客户的服务器上后,发现了一个非常头疼的问题,那么就是IIS7的应用程序池是集成模式的话,ASP.NET项目中自定义的HttpModule会处理静态文件(.html ...

  9. 网站加载css/js/img等静态文件失败

    网站加载css/js/img等静态文件失败,报网站http服务器内部500错误.而服务器中静态文件存在且权限正常. 从浏览器中直接访问文件,出来乱码.这种问题原因在于iis中该网站mime配置报错,不 ...

  10. 【前端】Sublime text3 插件HTML/CSS/JS prettify 格式化代码

    1.首先安装插件 菜单的preference->packages control,然后输入install .. 回车,再输入HTML/CSS/JS prettify 再回车,重启后就可以了. 2 ...

随机推荐

  1. Java-Request对象是用来回去请求信息,得到页面的请求

    1.Request 1.1 request对象和response对象的原理(了解) request对象和response对象是由服务器创建的,我们来使用他们即可 request对象是用来回去请求信息, ...

  2. git 怎么将某个开发分支最近几次的提交合并成一次提交

    1. 切换到开发分支: git checkout dev 2. 运行交互式 rebase 命令,并指定要合并的提交数量(在这个例子中是最近的3次提交): git rebase -i HEAD~3 3. ...

  3. [oeasy]python0037_终端_terminal_电传打字机_tty_shell_控制台_console_发展历史

    换行回车 回忆上次内容 换行 和 回车 是两回事 换行 对应字节0x0A Line-Feed 水平 不动 垂直 向上喂纸 所以是 feed 回车 对应字节0x0D Carriage-Return 垂直 ...

  4. Day 1 - 二分

    整数二分 我们可以做到每次排除一半的答案,时间复杂度 \(O(\log n)\). long long l = L, r = R; while(l <= r) { long long mid = ...

  5. Arch Linux install i3-wm

    Arch Linux install i3-wm 简介 i3-wm 是一种动态的平铺式窗口管理器,它的设计目标是提供一个快速.简洁.可定制的桌面环境,适合开发者和高级用户使用.它有以下几个特点: 它使 ...

  6. JavaScript小面试~数组相关的方法和运用(学习笔记)

    1,稀疏数组 稀疏数组是指数组中的某个下标未给出值或某个下标的值被删除.例如: let arrayOne=['xiaozi',,12,,true,23] let arrayTwo=[1,2,3,3,4 ...

  7. 15、SpringMVC之常用组件及执行流程

    15.1.常用组件 15.1.1. DispatcherServlet DispatcherServlet 是前端控制器,由框架提供,不需要工程师开发: 作用:统一处理请求和响应,整个流程控制的中心, ...

  8. 【SQL】 牛客网SQL训练Part1 简单难度

    地址位置: https://www.nowcoder.com/exam/oj?difficulty=2 查找入职员工时间排名倒数第三的员工所有信息 -- 准备脚本 drop table if exis ...

  9. 【Git】Git拉取失败,报错超出内存,内存分配失败

    报错信息: Microsoft Windows [版本 6.1.7601] 版权所有 (c) 2009 Microsoft Corporation.保留所有权利. C:\Users\Administr ...

  10. NVIDIA的Isaac AMR产品介绍

    NVIDIA的Isaac AMR是仓库自动运货机器人项目,说直白些就是一个AGV的小车,不过和传统的AGV不同,NVIDIA推出的这个产品是智能化的.传统AGV小车的运行代码都是写死的,直接把运行命令 ...