Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/src/assets/scss/component/_menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@
}

&__items {
max-height: 80vh;
overflow: auto;
padding: 0 8px;
}
Expand Down Expand Up @@ -367,7 +366,6 @@
&__submenu {
overflow: auto;
display: none;
max-height: 80vh;
border: 1px solid var(--b3-theme-surface-lighter);
border-radius: var(--b3-border-radius-b);
background-color: var(--b3-menu-background);
Expand Down
1 change: 1 addition & 0 deletions app/src/boot/onGetConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const onGetConfig = (isStart: boolean, app: App) => {
adjustLayout();
resizeTabs();
resizeTopBar();
window.siyuan.menus.menu.resetPosition();
firstResize = true;
if (getSelection().rangeCount > 0) {
const range = getSelection().getRangeAt(0);
Expand Down
62 changes: 50 additions & 12 deletions app/src/menus/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,47 @@ export class Menu {
});
}

public showSubMenu(subMenuElement: HTMLElement) {
public showSubMenu(subMenuElement: HTMLElement | null) {
if (!subMenuElement) {
return;
}
const itemsMenuElement = subMenuElement.lastElementChild as HTMLElement;
if (itemsMenuElement) {
itemsMenuElement.style.maxHeight = "";
}
const itemRect = subMenuElement.parentElement.getBoundingClientRect();
subMenuElement.style.top = (itemRect.top - 8) + "px";
subMenuElement.style.left = (itemRect.right + 8) + "px";
subMenuElement.style.bottom = "auto";
const rect = subMenuElement.getBoundingClientRect();
if (rect.right > window.innerWidth) {
if (itemRect.left - 8 > rect.width) {
subMenuElement.style.left = (itemRect.left - 8 - rect.width) + "px";
const subMenuRect = subMenuElement.getBoundingClientRect();

// 垂直方向位置调整
// 减 9px 是为了尽量对齐菜单选项(b3-menu__submenu 的默认 padding-top 加上子菜单首个 b3-menu__item 的默认 margin-top)
// 减 1px 是为了避免在特定情况下渲染出不应存在的滚动条而做的兼容处理
const top = Math.min(itemRect.top - 9, window.innerHeight - subMenuRect.height - 1);
subMenuElement.style.top = Math.max(Constants.SIZE_TOOLBAR_HEIGHT, top) + "px";

// 水平方向位置调整
if (subMenuRect.right <= window.innerWidth) {
// 8px 是 b3-menu__items 的默认 padding-right
subMenuElement.style.left = (itemRect.right + 8) + "px";
Comment on lines +81 to +83
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In showSubMenu, the horizontal positioning logic reads subMenuRect (line 72) before setting any left/top styles. The condition at line 81 (subMenuRect.right <= window.innerWidth) checks whether the submenu fits within the window at its current (stale) position, not at the intended placement of itemRect.right + 8. This can cause incorrect positioning:

  • If the submenu has no inline left yet (e.g., first show), its default fixed position might have subMenuRect.right within the window width, so the code takes the if branch and sets left = itemRect.right + 8. But the submenu at itemRect.right + 8 + subMenuRect.width might still overflow the right edge of the window.

The correct check should compare the intended position against the viewport width: itemRect.right + 8 + subMenuRect.width > window.innerWidth. This is what the old code did (set the position first, then read the rect).

Suggested change
if (subMenuRect.right <= window.innerWidth) {
// 8px 是 b3-menu__items 的默认 padding-right
subMenuElement.style.left = (itemRect.right + 8) + "px";
// 8px 是 b3-menu__items 的默认 padding-right
const intendedLeft = itemRect.right + 8;
const intendedRight = intendedLeft + subMenuRect.width;
if (intendedRight <= window.innerWidth) {
subMenuElement.style.left = intendedLeft + "px";

Copilot uses AI. Check for mistakes.
} else {
if (itemRect.left - 8 > subMenuRect.width) {
subMenuElement.style.left = (itemRect.left - 8 - subMenuRect.width) + "px";
} else {
subMenuElement.style.left = (window.innerWidth - rect.width) + "px";
subMenuElement.style.left = (window.innerWidth - subMenuRect.width) + "px";
}
}
if (rect.bottom > window.innerHeight) {
subMenuElement.style.top = "auto";
subMenuElement.style.bottom = "8px";

this.updateMaxHeight(subMenuElement, itemsMenuElement);
}

private updateMaxHeight(menuElement: HTMLElement, itemsMenuElement: HTMLElement) {
if (!menuElement || !itemsMenuElement) {
return;
}
const menuRect = menuElement.getBoundingClientRect();
const itemsMenuRect = itemsMenuElement.getBoundingClientRect();
// 加 1px 是为了避免在特定情况下渲染出不应存在的滚动条而做的兼容处理
const availableHeight = (window.innerHeight - menuRect.top) - (menuRect.height - itemsMenuRect.height) + 1;
itemsMenuElement.style.maxHeight = Math.max(availableHeight, 0) + "px";
}

private preventDefault(event: KeyboardEvent) {
Expand Down Expand Up @@ -149,6 +173,20 @@ export class Menu {
this.element.style.zIndex = (++window.siyuan.zIndex).toString();
this.element.classList.remove("fn__none");
setPosition(this.element, options.x - (options.isLeft ? this.element.clientWidth : 0), options.y, options.h, options.w);
this.updateMaxHeight(this.element, this.element.lastElementChild as HTMLElement);
}

public resetPosition() {
if (this.element.classList.contains("fn__none")) {
return;
}
setPosition(this.element, parseFloat(this.element.style.left), parseFloat(this.element.style.top), 0, 0); // 如果不存在 left 或 top,则得到 NaN
this.updateMaxHeight(this.element, this.element.lastElementChild as HTMLElement);
const subMenuElements = this.element.querySelectorAll(".b3-menu__item--show .b3-menu__submenu") as NodeListOf<HTMLElement>;
subMenuElements.forEach((subMenuElement) => {
// 可能有多层子菜单,都要重新定位
this.showSubMenu(subMenuElement);
});
}

public fullscreen(position: "bottom" | "all" = "all") {
Expand Down
51 changes: 31 additions & 20 deletions app/src/util/setPosition.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import {Constants} from "../constants";

export const setPosition = (element: HTMLElement, x: number, y: number, targetHeight = 0, targetLeft = 0) => {
element.style.top = y + "px";
element.style.left = x + "px";
export const setPosition = (element: HTMLElement, left: number, top: number, targetHeight = 0, targetLeft = 0) => {
const isTopValid = !isNaN(top); // 存在 top 时调整垂直方向位置
const isLeftValid = !isNaN(left); // 存在 left 时调整水平方向位置
if (isTopValid) {
element.style.top = top + "px";
}
if (isLeftValid) {
element.style.left = left + "px";
}
const rect = element.getBoundingClientRect();
// 上下超出屏幕
if (rect.bottom > window.innerHeight || rect.top < Constants.SIZE_TOOLBAR_HEIGHT) {
const top = y - rect.height - targetHeight;
if (top > Constants.SIZE_TOOLBAR_HEIGHT && (top + rect.height) < window.innerHeight) {
// 上部
element.style.top = top + "px";
} else if (top <= Constants.SIZE_TOOLBAR_HEIGHT) {
// 位置超越到屏幕上方外时,需移动到屏幕顶部。eg:光标在第一个块,然后滚动到上方看不见的位置,按 ctrl+a

if (isTopValid) {
if (rect.top < Constants.SIZE_TOOLBAR_HEIGHT) {
// 如果元素接触顶栏,向下移
element.style.top = Constants.SIZE_TOOLBAR_HEIGHT + "px";
} else {
// 依旧展现在下部,只是位置上移
element.style.top = Math.max(Constants.SIZE_TOOLBAR_HEIGHT, window.innerHeight - rect.height) + "px";
} else if (rect.bottom > window.innerHeight) {
// 如果元素底部超出窗口(下方空间不够),向上移
if (top - Constants.SIZE_TOOLBAR_HEIGHT >= rect.height) {
// 如果上方空间足够,向上移
element.style.top = (top - rect.height - targetHeight) + "px";
} else {
// 如果上下空间都不够,向上移,但尽量靠底部
element.style.top = Math.max(Constants.SIZE_TOOLBAR_HEIGHT, window.innerHeight - rect.height) + "px";
}
}
}
if (rect.right > window.innerWidth) {
// 展现在左侧
element.style.left = `${window.innerWidth - rect.width - targetLeft}px`;
} else if (rect.left < 0) {
// 依旧展现在左侧,只是位置右移
element.style.left = "0";

if (isLeftValid) {
if (rect.right > window.innerWidth) {
// 展现在左侧
element.style.left = window.innerWidth - rect.width - targetLeft + "px";
} else if (rect.left < 0) {
// 依旧展现在左侧,只是位置右移
element.style.left = "0";
}
}
};
1 change: 1 addition & 0 deletions app/src/window/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const init = (app: App) => {
resizeTimeout = window.setTimeout(() => {
adjustLayout(window.siyuan.layout.centerLayout);
resizeTabs();
window.siyuan.menus.menu.resetPosition();
if (getSelection().rangeCount > 0) {
const range = getSelection().getRangeAt(0);
getAllEditor().forEach(item => {
Expand Down