Skip to content

Commit 00dda41

Browse files
authored
🎨 Improve automatic scrolling when dragging items in the outline (#16161)
fix #15846 (comment)
1 parent 0207316 commit 00dda41

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

app/src/boot/globalEvent/dragover.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ export const cancelDrag = () => {
1818
}
1919
ghostElement.remove();
2020
document.onmousemove = null;
21+
// 通知取消拖拽,供相关模块停止滚动动画等
22+
window.dispatchEvent(new CustomEvent("drag-cancel"));
2123
}
2224
};

app/src/layout/dock/Outline.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export class Outline extends Model {
3636
public blockId: string;
3737
public isPreview: boolean;
3838
private preFilterExpandIds: string[] | null = null;
39+
private scrollAnimationId: number | null = null;
40+
private scrollLastFrameTime: number = 0;
41+
private scrollCurrentFPS: number = 60;
3942

4043
constructor(options: {
4144
app: App,
@@ -331,6 +334,21 @@ export class Outline extends Model {
331334
}, response => {
332335
this.update(response);
333336
});
337+
338+
window.addEventListener("drag-cancel", () => {
339+
this.stopScrollAnimation();
340+
});
341+
}
342+
343+
private stopScrollAnimation() {
344+
if (this.scrollAnimationId) {
345+
if (typeof cancelAnimationFrame !== "undefined") {
346+
cancelAnimationFrame(this.scrollAnimationId);
347+
} else {
348+
clearTimeout(this.scrollAnimationId);
349+
}
350+
this.scrollAnimationId = null;
351+
}
334352
}
335353

336354
private bindSort() {
@@ -368,18 +386,68 @@ export class Outline extends Model {
368386
}
369387
ghostElement.style.top = moveEvent.clientY + "px";
370388
ghostElement.style.left = moveEvent.clientX + "px";
389+
// 检查是否在滚动边界区域
390+
if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) {
391+
// 如果还没有开始滚动,则开始持续滚动
392+
if (!this.scrollAnimationId) {
393+
const scrollDirection = moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -1 : 1;
394+
this.scrollLastFrameTime = performance.now();
395+
let scrollFrameCount = 0;
396+
397+
const scrollAnimation = (currentTime: number) => {
398+
if (!this.scrollAnimationId) {
399+
return;
400+
}
401+
402+
// 每隔 20 帧重新计算一次帧率
403+
if (scrollFrameCount % 20 === 0) {
404+
const deltaTime = currentTime - this.scrollLastFrameTime;
405+
this.scrollLastFrameTime = currentTime;
406+
// 计算过去 20 帧的平均帧率
407+
this.scrollCurrentFPS = deltaTime > 0 ? (20 * 1000) / deltaTime : 60;
408+
}
409+
scrollFrameCount++;
410+
411+
// 基于当前帧率计算滚动步长,确保等效于 60fps 时的 16px/帧
412+
const baseScrollStep = 16;
413+
const targetFPS = 60;
414+
const scrollStep = baseScrollStep * (targetFPS / this.scrollCurrentFPS);
415+
416+
this.element.scroll({
417+
top: this.element.scrollTop + scrollStep * scrollDirection
418+
});
419+
420+
// 使用 requestAnimationFrame 继续动画
421+
this.scrollAnimationId = requestAnimationFrame(scrollAnimation);
422+
};
423+
424+
// 检查浏览器是否支持 requestAnimationFrame
425+
if (typeof requestAnimationFrame !== "undefined") {
426+
this.scrollAnimationId = requestAnimationFrame(scrollAnimation);
427+
} else {
428+
// 回退到 setTimeout 方法
429+
const scrollInterval = 16; // 约 60fps
430+
const scrollStep = 16; // 每次滚动的距离
431+
432+
const scrollAnimationFallback = () => {
433+
this.element.scroll({
434+
top: this.element.scrollTop + scrollStep * scrollDirection
435+
});
436+
this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval);
437+
};
438+
this.scrollAnimationId = window.setTimeout(scrollAnimationFallback, scrollInterval);
439+
}
440+
}
441+
} else {
442+
// 离开滚动区域时停止滚动
443+
this.stopScrollAnimation();
444+
}
371445
if (!this.element.contains(moveEvent.target as Element)) {
372446
this.element.querySelectorAll(".dragover__top, .dragover__bottom, .dragover, .dragover__current").forEach(item => {
373447
item.classList.remove("dragover__top", "dragover__bottom", "dragover", "dragover__current");
374448
});
375449
return;
376450
}
377-
if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) {
378-
this.element.scroll({
379-
top: this.element.scrollTop + (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB ? -Constants.SIZE_SCROLL_STEP : Constants.SIZE_SCROLL_STEP),
380-
behavior: "smooth"
381-
});
382-
}
383451
selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement;
384452
if (!selectItem || selectItem.tagName !== "LI" || selectItem.style.position === "fixed") {
385453
return;
@@ -410,6 +478,8 @@ export class Outline extends Model {
410478
documentSelf.onselect = null;
411479
ghostElement?.remove();
412480
item.style.opacity = "";
481+
// 清理滚动动画
482+
this.stopScrollAnimation();
413483
if (!selectItem) {
414484
selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover");
415485
}

0 commit comments

Comments
 (0)