🎨 Improve automatic scrolling when dragging items in the outline (#15846)

This commit is contained in:
Jeffrey Chen 2025-10-20 11:15:39 +08:00 committed by GitHub
parent 950f392217
commit b3b5bbbc2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -36,6 +36,9 @@ export class Outline extends Model {
public blockId: string;
public isPreview: boolean;
private preFilterExpandIds: string[] | null = null;
private scrollAnimationId: number | null = null;
private scrollLastFrameTime: number = 0;
private scrollCurrentFPS: number = 60;
constructor(options: {
app: App,
@ -374,11 +377,68 @@ export class Outline extends Model {
});
return;
}
// 检查是否在滚动边界区域
if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) {
this.element.scroll({
top: this.element.scrollTop + (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -Constants.SIZE_SCROLL_STEP : Constants.SIZE_SCROLL_STEP),
behavior: "smooth"
});
// 如果还没有开始滚动,则开始持续滚动
if (!this.scrollAnimationId) {
const scrollDirection = moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -1 : 1;
this.scrollLastFrameTime = performance.now();
let scrollFrameCount = 0;
const scrollAnimation = (currentTime: number) => {
if (!this.scrollAnimationId) {
return;
}
// 每隔 20 帧重新计算一次帧率
if (scrollFrameCount % 20 === 0) {
const deltaTime = currentTime - this.scrollLastFrameTime;
this.scrollLastFrameTime = currentTime;
// 计算过去 20 帧的平均帧率
this.scrollCurrentFPS = deltaTime > 0 ? (20 * 1000) / deltaTime : 60;
}
scrollFrameCount++;
// 基于当前帧率计算滚动步长,确保等效于 60fps 时的 16px/帧
const baseScrollStep = 16;
const targetFPS = 60;
const scrollStep = baseScrollStep * (targetFPS / this.scrollCurrentFPS);
this.element.scroll({
top: this.element.scrollTop + scrollStep * scrollDirection
});
// 使用 requestAnimationFrame 继续动画
this.scrollAnimationId = requestAnimationFrame(scrollAnimation);
};
// 检查浏览器是否支持 requestAnimationFrame
if (typeof requestAnimationFrame !== "undefined") {
this.scrollAnimationId = requestAnimationFrame(scrollAnimation);
} else {
// 回退到 setTimeout 方法
const scrollInterval = 16; // 约 60fps
const scrollStep = 16; // 每次滚动的距离
const scrollAnimationFallback = () => {
this.element.scroll({
top: this.element.scrollTop + scrollStep * scrollDirection
});
this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval);
};
this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval);
}
}
} else {
// 离开滚动区域时停止滚动
if (this.scrollAnimationId) {
if (typeof cancelAnimationFrame !== "undefined") {
cancelAnimationFrame(this.scrollAnimationId);
} else {
clearTimeout(this.scrollAnimationId);
}
this.scrollAnimationId = null;
}
}
selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement;
if (!selectItem || selectItem.tagName !== "LI" || selectItem.style.position === "fixed") {
@ -410,6 +470,15 @@ export class Outline extends Model {
documentSelf.onselect = null;
ghostElement?.remove();
item.style.opacity = "";
// 清理滚动动画
if (this.scrollAnimationId) {
if (typeof cancelAnimationFrame !== "undefined") {
cancelAnimationFrame(this.scrollAnimationId);
} else {
clearTimeout(this.scrollAnimationId);
}
this.scrollAnimationId = null;
}
if (!selectItem) {
selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover");
}