引文
今天大脑闪回以前好像有个需求,就是在一个内容区域内,鼠标移出自定义滚动条隐藏,移入则展示滚动条,当时开发的时候2个思路。1.监听鼠标移入移出来用样式控制原生滚动条的展示,用了display:none/block发现布局抖动,用hover:hidden/overflow也没办法 2.自定义实现一个滚动条去控制显示隐藏,然后没实现出来(菜的抠脚),然而到ai发展的今天发现无论是思路1还是2都让ai实现了一次,而我被ai替代了2次(damn!)
提问:你作为一个前端开发工程师,实现一个自定义滚动条,鼠标移过去滚动条自然展示,鼠标移开滚动条消失,布局不可以变化,用原生js实现下
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Scrollbar</title>
<style>
.container {
width: 500px;
height: 300px;
overflow: hidden;
position: relative;
}
.content {
width: 100%;
height: 100%;
overflow: auto;
padding: 10px;
}
.custom-scrollbar {
position: absolute;
top: 0;
right: 0;
width: 8px;
background: transparent;
z-index: 10;
transition: opacity 0.3s;
}
.scrollbar-thumb {
position: absolute;
top: 0;
width: 100%;
background: #888;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container" id="container">
<div class="content" id="content">
<!-- 生成100行示例文本 -->
<div style="height: 2000px;">
<h3>自定义滚动条示例</h3>
<p>鼠标移入右侧区域显示滚动条,移出隐藏</p>
<p>当前时间:<span id="time"></span></p>
<!-- 重复内容 -->
<div style="height: 1500px;">
<p>滚动内容区域...</p>
</div>
</div>
</div>
<div class="custom-scrollbar" id="customScrollbar"></div>
</div>
<script>
class CustomScrollbar {
constructor(containerId, scrollbarId, contentId) {
this.container = document.getElementById(containerId);
this.scrollbar = document.getElementById(scrollbarId);
this.content = document.getElementById(contentId);
this.isMouseOver = false;
this.isDragging = false;
this.scrollPosition = 0;
this.init();
}
init() {
this.createScrollbar();
this.bindEvents();
this.updateScrollbar();
}
createScrollbar() {
const thumb = document.createElement('div');
thumb.className = 'scrollbar-thumb';
this.scrollbar.appendChild(thumb);
this.thumb = thumb;
}
bindEvents() {
// 容器鼠标事件
this.container.addEventListener('mouseenter', () => this.showScrollbar());
this.container.addEventListener('mouseleave', () => this.hideScrollbar());
// 滚动条鼠标事件
this.scrollbar.addEventListener('mouseenter', () => this.showScrollbar());
this.scrollbar.addEventListener('mouseleave', () => this.hideScrollbar());
// 滚动内容事件
this.content.addEventListener('scroll', () => this.updateScrollbar());
// 滑块拖动事件
this.thumb.addEventListener('mousedown', (e) => this.startDrag(e));
document.addEventListener('mousemove', (e) => this.onDrag(e));
document.addEventListener('mouseup', () => this.stopDrag());
// 滚轮事件
this.container.addEventListener('wheel', (e) => this.handleWheel(e));
}
showScrollbar() {
this.scrollbar.style.opacity = '1';
this.isMouseOver = true;
}
hideScrollbar() {
if (!this.isDragging) {
this.scrollbar.style.opacity = '0';
this.isMouseOver = false;
}
}
updateScrollbar() {
const contentHeight = this.content.scrollHeight;
const visibleHeight = this.content.clientHeight;
const scrollTop = this.content.scrollTop;
// 计算滑块高度
const thumbHeight = Math.max(20, (visibleHeight / contentHeight) * visibleHeight);
this.thumb.style.height = `${thumbHeight}px`;
// 计算滑块位置
const thumbPosition = (scrollTop / (contentHeight - visibleHeight)) *
(visibleHeight - thumbHeight);
this.thumb.style.top = `${thumbPosition}px`;
this.scrollPosition = scrollTop;
}
startDrag(e) {
e.preventDefault();
this.isDragging = true;
this.dragStartY = e.clientY;
this.dragStartTop = parseInt(this.thumb.style.top) || 0;
this.scrollbar.style.opacity = '1';
}
onDrag(e) {
if (!this.isDragging) return;
const deltaY = e.clientY - this.dragStartY;
const newTop = this.dragStartTop + deltaY;
// 限制滑块在有效范围内
const maxTop = this.container.clientHeight - this.thumb.clientHeight;
const clampedTop = Math.max(0, Math.min(newTop, maxTop));
this.thumb.style.top = `${clampedTop}px`;
// 计算并设置内容滚动位置
const contentHeight = this.content.scrollHeight;
const visibleHeight = this.content.clientHeight;
const scrollPosition = (clampedTop / (maxTop || 1)) *
(contentHeight - visibleHeight);
this.content.scrollTop = scrollPosition;
this.scrollPosition = scrollPosition;
}
stopDrag() {
this.isDragging = false;
if (!this.isMouseOver) {
this.scrollbar.style.opacity = '0';
}
}
handleWheel(e) {
if (!this.isMouseOver && !this.isDragging) return;
e.preventDefault();
const delta = e.deltaY > 0 ? 50 : -50;
this.content.scrollTop += delta;
this.updateScrollbar();
}
}
// 初始化
const scrollbar = new CustomScrollbar('container', 'customScrollbar', 'content');
// 更新时间显示
function updateTime() {
document.getElementById('time').textContent = new Date().toLocaleTimeString();
}
setInterval(updateTime, 1000);
updateTime();
</script>
</body>
</html>
进而我在思考能不能抽离成一个js库方便调用这个功能
提问:用js封装成一个库,方便前端调用
js
// custom-scrollbar.js
class CustomScrollbar {
constructor(options) {
// 合并默认配置和用户配置
this.options = {
container: null,
content: null,
width: 8,
thumbColor: '#888',
thumbHoverColor: '#555',
thumbBorderRadius: 4,
autoHide: true,
hideDelay: 2000,
...options
};
// 验证必需参数
if (!this.options.container || !this.options.content) {
throw new Error('必须提供container和content元素');
}
this.container = this.options.container;
this.content = this.options.content;
this.isMouseOver = false;
this.isDragging = false;
this.hideTimer = null;
this.init();
}
init() {
// 确保容器是相对定位
this.container.style.position = 'relative';
// 创建滚动条轨道
this.scrollbar = document.createElement('div');
this.scrollbar.className = 'custom-scrollbar-track';
this.scrollbar.style.position = 'absolute';
this.scrollbar.style.top = '0';
this.scrollbar.style.right = '0';
this.scrollbar.style.width = `${this.options.width}px`;
this.scrollbar.style.backgroundColor = 'transparent';
this.scrollbar.style.zIndex = '10';
this.scrollbar.style.transition = 'opacity 0.3s';
this.scrollbar.style.opacity = this.options.autoHide ? '0' : '1';
// 创建滑块
this.thumb = document.createElement('div');
this.thumb.className = 'custom-scrollbar-thumb';
this.thumb.style.position = 'absolute';
this.thumb.style.top = '0';
this.thumb.style.width = '100%';
this.thumb.style.backgroundColor = this.options.thumbColor;
this.thumb.style.borderRadius = `${this.options.thumbBorderRadius}px`;
this.thumb.style.cursor = 'pointer';
this.scrollbar.appendChild(this.thumb);
this.container.appendChild(this.scrollbar);
this.bindEvents();
this.updateScrollbar();
}
bindEvents() {
// 容器鼠标事件
this.container.addEventListener('mouseenter', () => this.showScrollbar());
this.container.addEventListener('mouseleave', () => this.hideScrollbar());
// 滚动条鼠标事件
this.scrollbar.addEventListener('mouseenter', () => this.showScrollbar());
this.scrollbar.addEventListener('mouseleave', () => this.hideScrollbar());
// 滚动内容事件
this.content.addEventListener('scroll', () => this.updateScrollbar());
// 滑块事件
this.thumb.addEventListener('mousedown', (e) => this.startDrag(e));
// 滚轮事件
this.container.addEventListener('wheel', (e) => this.handleWheel(e), { passive: false });
}
showScrollbar() {
clearTimeout(this.hideTimer);
this.hideTimer = null;
this.scrollbar.style.opacity = '1';
this.isMouseOver = true;
}
hideScrollbar() {
if (!this.options.autoHide || this.isDragging) return;
this.hideTimer = setTimeout(() => {
this.scrollbar.style.opacity = '0';
this.isMouseOver = false;
}, this.options.hideDelay);
}
updateScrollbar() {
const contentHeight = this.content.scrollHeight;
const visibleHeight = this.content.clientHeight;
const scrollTop = this.content.scrollTop;
// 计算滑块高度(最小20px)
const thumbHeight = Math.max(20, (visibleHeight / contentHeight) * visibleHeight);
this.thumb.style.height = `${thumbHeight}px`;
// 计算滑块位置
const maxScroll = contentHeight - visibleHeight;
const maxThumbPosition = visibleHeight - thumbHeight;
const thumbPosition = (scrollTop / maxScroll) * maxThumbPosition;
this.thumb.style.top = `${thumbPosition}px`;
}
startDrag(e) {
e.preventDefault();
this.isDragging = true;
this.dragStartY = e.clientY;
this.dragStartTop = parseInt(this.thumb.style.top) || 0;
this.scrollbar.style.opacity = '1';
// 添加全局事件监听
document.addEventListener('mousemove', this.onDrag);
document.addEventListener('mouseup', this.stopDrag);
}
onDrag = (e) => {
if (!this.isDragging) return;
const deltaY = e.clientY - this.dragStartY;
const newTop = this.dragStartTop + deltaY;
// 限制滑块在有效范围内
const maxTop = this.content.clientHeight - this.thumb.clientHeight;
const clampedTop = Math.max(0, Math.min(newTop, maxTop));
this.thumb.style.top = `${clampedTop}px`;
// 计算并设置内容滚动位置
const contentHeight = this.content.scrollHeight;
const visibleHeight = this.content.clientHeight;
const scrollPosition = (clampedTop / maxTop) * (contentHeight - visibleHeight);
this.content.scrollTop = scrollPosition;
}
stopDrag = () => {
this.isDragging = false;
document.removeEventListener('mousemove', this.onDrag);
document.removeEventListener('mouseup', this.stopDrag);
this.hideScrollbar();
}
handleWheel(e) {
if (!this.isMouseOver && !this.isDragging) {
e.preventDefault();
return;
}
e.preventDefault();
const delta = e.deltaY > 0 ? 50 : -50;
this.content.scrollTop += delta;
this.updateScrollbar();
}
// 销毁实例,清理DOM和事件监听
destroy() {
clearTimeout(this.hideTimer);
this.scrollbar.remove();
// 移除所有事件监听
this.container.removeEventListener('mouseenter', this.showScrollbar);
this.container.removeEventListener('mouseleave', this.hideScrollbar);
this.scrollbar.removeEventListener('mouseenter', this.showScrollbar);
this.scrollbar.removeEventListener('mouseleave', this.hideScrollbar);
this.content.removeEventListener('scroll', this.updateScrollbar);
this.thumb.removeEventListener('mousedown', this.startDrag);
this.container.removeEventListener('wheel', this.handleWheel);
document.removeEventListener('mousemove', this.onDrag);
document.removeEventListener('mouseup', this.stopDrag);
}
}
// 导出库函数
function initCustomScrollbar(options) {
return new CustomScrollbar(options);
}
// 导出到全局作用域(UMD模式)
if (typeof module !== 'undefined' && module.exports) {
module.exports = initCustomScrollbar;
} else if (typeof define === 'function' && define.amd) {
define([], function() {
return initCustomScrollbar;
});
} else {
window.initCustomScrollbar = initCustomScrollbar;
}
使用方法:
- 引入库文件:
xml
html
<script src="path/to/custom-scrollbar.js"></script>
- 初始化滚动条:
php
javascript
// 基本用法
const myScrollbar = initCustomScrollbar({
container: document.getElementById('container'),
content: document.getElementById('content')
});
// 高级用法(带配置)
const myScrollbar = initCustomScrollbar({
container: document.querySelector('.scroll-container'),
content: document.querySelector('.scroll-content'),
width: 10,
thumbColor: '#ff5722',
thumbHoverColor: '#e64a19',
thumbBorderRadius: 5,
autoHide: false,
hideDelay: 3000
});
- 销毁实例(当不需要时):
ini
javascript
myScrollbar.destroy();
配置参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| container | HTMLElement | 必填 | 滚动容器元素 |
| content | HTMLElement | 必填 | 滚动内容元素 |
| width | number | 8 | 滚动条宽度(像素) |
| thumbColor | string | #888 | 滑块颜色 |
| thumbHoverColor | string | #555 | 滑块悬停颜色 |
| thumbBorderRadius | number | 4 | 滑块圆角 |
| autoHide | boolean | true | 是否自动隐藏滚动条 |
| hideDelay | number | 2000 | 自动隐藏延迟(毫秒) |
特点:
- 灵活配置:支持高度定制化配置
- 内存安全:提供destroy方法清理资源
- 兼容性:支持CommonJS、AMD和全局变量三种引入方式
- 性能优化:使用passive事件监听提升滚动性能
- 平滑动画:使用CSS transition实现平滑的显示/隐藏效果
- 响应式:自动适应容器大小变化
示例代码:
html
<!DOCTYPE html>
<html>
<head>
<style>
.container {
width: 500px;
height: 300px;
position: relative;
border: 1px solid #ccc;
}
.content {
width: 100%;
height: 100%;
padding: 10px;
overflow: auto;
}
</style>
</head>
<body>
<div class="container" id="container">
<div class="content" id="content">
<!-- 长内容区域 -->
<div style="height: 1500px;">
<h3>自定义滚动条库示例</h3>
<p>这是一个使用自定义滚动条库的示例页面</p>
<!-- 更多内容 -->
</div>
</div>
</div>
<script src="custom-scrollbar.js"></script>
<script>
// 初始化滚动条
const scrollbar = initCustomScrollbar({
container: document.getElementById('container'),
content: document.getElementById('content'),
width: 12,
thumbColor: '#4CAF50',
thumbHoverColor: '#388E3C',
thumbBorderRadius: 6,
hideDelay: 3000
});
// 窗口大小变化时更新
window.addEventListener('resize', () => {
scrollbar.updateScrollbar();
});
</script>
</body>
</html>
这个库提供了完整的自定义滚动条功能,支持多种配置选项,并且可以方便地集成到任何前端项目中。通过提供destroy方法,确保可以正确清理资源,避免内存泄漏。
ai的回答不但给了实现还贴心给了文档和示例
然后我在思考用控制原生滚动条的方式,它会如何给我答案(虽然第一次给了我错误的答案,但是第二次询问下)
提问:通过控制原生的滚动条样式实现鼠标移入展示原生滚动条,移出隐藏原生滚动条嘛,原生js和css实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>原生滚动条动态显示/隐藏</title>
<style>
.container {
width: 500px;
height: 300px;
position: relative;
overflow: hidden; /* 默认隐藏滚动条 */
box-sizing: border-box;
border: 1px solid #ccc;
padding-right: 15px; /* 预留滚动条空间 */
}
.content {
width: 100%;
height: 100%;
overflow: auto;
padding: 10px;
}
/* Chrome/Safari 隐藏滚动条 */
.content::-webkit-scrollbar {
display: none;
}
/* 鼠标移入时显示滚动条 */
.container:hover .content {
overflow-y: auto; /* 显示垂直滚动条 */
}
/* 针对 Chrome/Safari 显示滚动条 */
.container:hover .content::-webkit-scrollbar {
display: block;
width: 15px;
}
/* 滚动条滑块样式 */
.container:hover .content::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 1px;
}
/* 滚动条轨道样式 */
.container:hover .content::-webkit-scrollbar-track {
background: transparent;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div style="height: 1500px;">
<h3>原生滚动条动态控制示例</h3>
<p>鼠标移入右侧区域显示滚动条,移出隐藏</p>
<p>当前时间:<span id="time"></span></p>
<p>滚动内容区域...</p>
<div style="height: 1000px;">
<p>更多内容...</p>
</div>
</div>
</div>
</div>
<script>
// 更新时间显示
function updateTime() {
document.getElementById('time').textContent = new Date().toLocaleTimeString();
}
setInterval(updateTime, 1000);
updateTime();
// 监听容器鼠标事件
const container = document.querySelector('.container');
const content = document.querySelector('.content');
container.addEventListener('mouseenter', () => {
// 显示滚动条
content.style.overflowY = 'auto';
// 针对 Chrome/Safari 显示滚动条
content.style.webkitOverflowScrolling = 'touch';
});
container.addEventListener('mouseleave', () => {
// 隐藏滚动条
content.style.overflowY = 'hidden';
// 针对 Chrome/Safari 隐藏滚动条
content.style.webkitOverflowScrolling = 'auto';
});
</script>
</body>
</html>
总结:
1.现在ai真的非常强大,可以帮你提供思路实现大概逻辑
2.你必须学会如何正确引导ai给你答案,正确的提问
3.君子善假于物也,世界上唯一不变的事情就是世界一直在日新月异的变化