console
class ListView extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "closed" });
this.adpater = null;
this.container = document.createElement("div");
this.container.style.cssText = `overflow: hidden;width: 100%;
height: 100%;position: relative;`;
this.content = document.createElement("div");
this.content.style.cssText = `width: 100%; height: 100%;
position:absolute;left: 0;top: 0;user-select: none;`;
this.container.appendChild(this.content);
this.scrollbar = document.createElement("div");
this.scrollbar.style.cssText = `position: absolute; right: 0;
top: 0; height: 100%;user-select: none;`;
this.scrollbarThumb = document.createElement("div");
this.scrollbarThumb.style.cssText = `width: 100%; background: #ccc;cursor:
pointer;position: absolute;top: 0; left: 0;user-select: none;
box-sizing: border-box;`;
this.scrollbar.appendChild(this.scrollbarThumb);
this.container.appendChild(this.scrollbar);
this.shadow.appendChild(this.container);
this._firstRow = null;
this._lastRow = null;
this._elements = [];
this._scrollTop = 0;
this._pending = false;
this._keepThumbActive = false;
this._isMouseOnScrollbarThumb = false;
this._observer = new ResizeObserver((es) => {
this.schedule();
});
this._observer.observe(this.container);
this._scrolbarSize = 15;
Object.defineProperty(this, "scrollbarSize", {
enumerable: true,
configurable: true,
get() {
return this._scrolbarSize;
},
set(val) {
if (typeof val != "number" || !val) {
return;
}
val = Math.max(0, val);
this._scrolbarSize = val;
this.scrollbar.style.width = val + "px";
this.content.style.width = `calc(100% - ${val}px)`;
}
});
this.scrollbarSize = 10;
let _self = this;
function captureMouse(e) {
let thumbRect = _self.scrollbarThumb.getBoundingClientRect();
let thumbOffsetY = e.clientY - thumbRect.top;
let rect = _self.scrollbar.getBoundingClientRect();
function onMouseMove(e) {
let totalHeight = _self.adpater.getItemCount() * _self.adpater.getItemHeight();
let thumbOffsetTop = e.clientY - thumbOffsetY - rect.top;
let maxOffset = rect.height - thumbRect.height;
thumbOffsetTop = Math.min(maxOffset, Math.max(0, thumbOffsetTop));
_self.scrollbarThumb.style.top = thumbOffsetTop + "px";
_self._scrollTop = thumbOffsetTop * (totalHeight - rect.height) / maxOffset;
_self.schedule();
}
function onMouseUp(e) {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
_self._keepThumbActive = false;
if(!_self._isMouseOnScrollbarThumb) {
_self.scrollbarThumb.style.background = "#ccc";
}
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
_self._keepThumbActive = true;
}
this.scrollbarThumb.addEventListener("mousedown", function (e) {
e.stopImmediatePropagation();
captureMouse(e);
});
this.scrollbarThumb.addEventListener("mouseenter", function (e) {
_self.scrollbarThumb.style.background = "#999";
_self._isMouseOnScrollbarThumb = true;
});
this.scrollbarThumb.addEventListener("mouseleave", function (e) {
_self._isMouseOnScrollbarThumb = false;
if(_self._keepThumbActive) {
return;
}
_self.scrollbarThumb.style.background = "#ccc";
});
let scrollFinished = true;
let animationScroll = true;
let timer = null;
let deltaY = 0;
function startAnimationScroll() {
_self.setScrollTopBy(deltaY);
let isNegative = deltaY < 0;
let newDeltaY = Math.abs(deltaY) - 0.5;
deltaY = isNegative ? -newDeltaY : newDeltaY;
if (newDeltaY <= 0) {
deltaY = 0;
scrollFinished = true;
cancelAnimationFrame(timer);
} else {
timer = requestAnimationFrame(startAnimationScroll);
}
}
function startAnimationWheelScroll() {
if (!scrollFinished) {
return;
}
scrollFinished = false;
startAnimationScroll();
}
this.container.addEventListener("wheel", function (e) {
let isTouch = Math.abs(e.deltaY) < 125;
if (animationScroll && !isTouch) {
deltaY = e.deltaY < 0 ? -15 : 15;
startAnimationWheelScroll();
} else {
_self.setScrollTopBy(e.deltaY);
}
let maxScrollTop = _self.getScrollHeight() - _self.container.getBoundingClientRect().height;
if(_self._scrollTop > 0 && _self._scrollTop < maxScrollTop) {
e.preventDefault();
}
});
function captureMouse2(e) {
let timer = null;
let rect = _self._rect;
let thumbRect = _self.scrollbarThumb.getBoundingClientRect();
let isMouseOnScrollbarTrack = true;
let dir = e.clientY > thumbRect.bottom ? 1 : 0;
function onMouseUp() {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
_self.scrollbar.removeEventListener("mouseenter", onMouseEnter);
_self.scrollbar.removeEventListener("mouseleave", onMouseLeave);
cancelAnimationFrame(timer);
}
function onMouseEnter() {
isMouseOnScrollbarTrack = true;
}
function onMouseLeave() {
isMouseOnScrollbarTrack = false;
}
function onMouseMove(ev) {
e = ev;
}
document.addEventListener("mouseup", onMouseUp);
document.addEventListener("mousemove", onMouseMove);
_self.scrollbar.addEventListener("mouseenter", onMouseEnter);
_self.scrollbar.addEventListener("mouseleave", onMouseLeave);
function startAutoScroll() {
let thumbRect = _self.scrollbarThumb.getBoundingClientRect();
let dir2 = e.clientY > thumbRect.bottom ? 1 : (e.clientY < thumbRect.top ? 0 : -1);
if (dir2 == dir) {
if (isMouseOnScrollbarTrack) {
let max = 25, min = 1;
let offset = Math.floor(Math.random() * (max - min)) + min;
let delta = dir2 == 0 ? -offset : (dir2 == 1 ? offset : 0);
_self.setScrollTopBy(delta);
}
}
timer = requestAnimationFrame(startAutoScroll);
}
startAutoScroll();
}
this.scrollbar.addEventListener("mousedown", function (e) {
captureMouse2(e);
});
}
getScrollHeight() {
return this.adpater ? this.adpater.getItemCount() * this.adpater.getItemHeight() : 0;
}
setAdapter(adapter) {
this.adpater = adapter;
this.schedule();
}
update() {
if (!this.adpater) {
return;
}
let itemHeight = this.adpater.getItemHeight();
let itemCount = this.adpater.getItemCount();
let totalHeight = itemCount * itemHeight;
let rect = this._rect || this.container.getBoundingClientRect();
let thumbHeight = Math.max(20, rect.height / totalHeight * rect.height);
this.scrollbarThumb.style.height = thumbHeight + "px";
this.content.style.top = -(this._scrollTop % this.adpater.getItemHeight()) + "px";
let firstRow = Math.floor(this._scrollTop / itemHeight);
let lastRow = Math.min(itemCount - 1, Math.ceil((this._scrollTop + rect.height) / itemHeight));
if (firstRow === this._firstRow && lastRow === this._lastRow) {
return;
}
this._firstRow = firstRow;
this._lastRow = lastRow;
let counter = 0;
for (let i = firstRow; i <= lastRow; ++i) {
let elem = this.adpater.getElement(i, this._elements[counter]);
this._elements[counter] = elem;
++counter;
}
this._elements.length = counter;
this.content.replaceChildren(...this._elements);
}
schedule(changes) {
if (this._pending) {
return;
}
this._pending = true;
requestAnimationFrame(() => {
this.update();
this._pending = false;
this._changes = 0;
});
}
setScrollTop(scrollTop) {
if (!this.adpater || typeof scrollTop != "number") {
return;
}
let scrollHeight = this.adpater.getItemHeight() * this.adpater.getItemCount();
let rect = this._rect || this.container.getBoundingClientRect();
let maxScrollTop = scrollHeight - rect.height;
scrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);
this._scrollTop = scrollTop;
let thumbHeight = this.scrollbarThumb.getBoundingClientRect().height;
let thumbOffsetTop = scrollTop * (rect.height - thumbHeight) / (scrollHeight - rect.height);
this.scrollbarThumb.style.top = thumbOffsetTop + "px";
this.schedule();
}
setScrollTopBy(delta) {
this.setScrollTop(this._scrollTop + delta);
}
}
customElements.define("list-view", ListView);
let listView = document.querySelector("list-view");
listView.setAdapter({
getItemHeight() {
return 70;
},
getItemCount() {
return 200000;
},
getElement(i, elem) {
if (!elem) {
elem = document.createElement("div");
elem.style.height = elem.style.lineHeight = this.getItemHeight() + "px";
elem.style.boxSizing = "border-box";
elem.style.padding = "10px";
let img = document.createElement("img");
img.src = "https://cn.bing.com/th?id=OPAC.wi8BGRAzJverPQ474C474&o=5&pid=21.1&w=160&h=160&qlt=100&dpr=1.3";
img.style.cssText = "height: 100%; float: left;";
elem.appendChild(img);
let price = document.createElement("div");
price.classList.add("price");
price.style.cssText = `float: left; margin-left: 10px;`;
elem.appendChild(price);
}
elem.querySelector(".price").textContent = "连衣裙" + (i + 1);
return elem;
}
});
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=, initial-scale=">
<meta http-equiv="X-UA-Compatible" content="">
<title></title>
</head>
<body>
<div id="container">
<list-view></list-view>
</div>
</body>
</html>
#container {
position: absolute;
width: 300px;
height: 300px;
box-shadow: 0 0 3px #aaa;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
body,html {
overflow: hidden;
}