From 00dda414dbf7c33233005fd641eb9a1f7a3a607a Mon Sep 17 00:00:00 2001 From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:58:59 +0800 Subject: [PATCH] :art: Improve automatic scrolling when dragging items in the outline (#16161) fix https://github.com/siyuan-note/siyuan/pull/15846#issuecomment-3420412057 --- app/src/boot/globalEvent/dragover.ts | 2 + app/src/layout/dock/Outline.ts | 82 ++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/app/src/boot/globalEvent/dragover.ts b/app/src/boot/globalEvent/dragover.ts index e6758ab9c..bb29e5163 100644 --- a/app/src/boot/globalEvent/dragover.ts +++ b/app/src/boot/globalEvent/dragover.ts @@ -18,5 +18,7 @@ export const cancelDrag = () => { } ghostElement.remove(); document.onmousemove = null; + // 通知取消拖拽,供相关模块停止滚动动画等 + window.dispatchEvent(new CustomEvent("drag-cancel")); } }; diff --git a/app/src/layout/dock/Outline.ts b/app/src/layout/dock/Outline.ts index 508d64fed..f30c4081f 100644 --- a/app/src/layout/dock/Outline.ts +++ b/app/src/layout/dock/Outline.ts @@ -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, @@ -331,6 +334,21 @@ export class Outline extends Model { }, response => { this.update(response); }); + + window.addEventListener("drag-cancel", () => { + this.stopScrollAnimation(); + }); + } + + private stopScrollAnimation() { + if (this.scrollAnimationId) { + if (typeof cancelAnimationFrame !== "undefined") { + cancelAnimationFrame(this.scrollAnimationId); + } else { + clearTimeout(this.scrollAnimationId); + } + this.scrollAnimationId = null; + } } private bindSort() { @@ -368,18 +386,68 @@ export class Outline extends Model { } ghostElement.style.top = moveEvent.clientY + "px"; ghostElement.style.left = moveEvent.clientX + "px"; + // 检查是否在滚动边界区域 + if (moveEvent.clientY < contentRect.top + Constants.SIZE_SCROLL_TB || moveEvent.clientY > contentRect.bottom - Constants.SIZE_SCROLL_TB) { + // 如果还没有开始滚动,则开始持续滚动 + 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 { + // 离开滚动区域时停止滚动 + this.stopScrollAnimation(); + } if (!this.element.contains(moveEvent.target as Element)) { this.element.querySelectorAll(".dragover__top, .dragover__bottom, .dragover, .dragover__current").forEach(item => { item.classList.remove("dragover__top", "dragover__bottom", "dragover", "dragover__current"); }); 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" - }); - } selectItem = hasClosestByClassName(moveEvent.target as HTMLElement, "b3-list-item") as HTMLElement; if (!selectItem || selectItem.tagName !== "LI" || selectItem.style.position === "fixed") { return; @@ -410,6 +478,8 @@ export class Outline extends Model { documentSelf.onselect = null; ghostElement?.remove(); item.style.opacity = ""; + // 清理滚动动画 + this.stopScrollAnimation(); if (!selectItem) { selectItem = this.element.querySelector(".dragover__top, .dragover__bottom, .dragover"); }