From 3641a342f4e4b8781875b95fa6c6829d2dd8c3bc Mon Sep 17 00:00:00 2001
From: Youzini-afk <13153778771cx@gmail.com>
Date: Sat, 28 Mar 2026 00:23:47 +0800
Subject: [PATCH] fix: adapt floating ball for mobile viewport
---
panel.js | 189 +++++++++++++++++++++++++++++++++++++++++++-----------
style.css | 19 ++++++
2 files changed, 170 insertions(+), 38 deletions(-)
diff --git a/panel.js b/panel.js
index 69deab5..9d1527e 100644
--- a/panel.js
+++ b/panel.js
@@ -183,48 +183,151 @@ function mountPanelHtml(html) {
document.documentElement?.appendChild(fragment);
}
-function ensureOverlayMountedAtRoot() {
- if (!overlayEl) return;
-
+function ensureNodeMountedAtRoot(node, { beforeBody = false } = {}) {
+ if (!node) return;
const root = document.documentElement;
const body = document.body;
if (!root) return;
- if (overlayEl.parentElement === root && overlayEl.nextElementSibling === body) {
+ if (beforeBody && body?.parentElement === root) {
+ if (node.parentElement === root && node.nextElementSibling === body) {
+ return;
+ }
+ root.insertBefore(node, body);
return;
}
- if (body?.parentElement === root) {
- root.insertBefore(overlayEl, body);
+ if (node.parentElement === root) {
return;
}
- root.appendChild(overlayEl);
+ root.appendChild(node);
+}
+
+function ensureOverlayMountedAtRoot() {
+ ensureNodeMountedAtRoot(overlayEl, { beforeBody: true });
+}
+
+function ensureFabMountedAtRoot() {
+ ensureNodeMountedAtRoot(_fabEl);
+}
+
+function getViewportMetrics() {
+ const viewport = window.visualViewport;
+ return {
+ width: Math.max(
+ 1,
+ Math.round(viewport?.width || window.innerWidth || 0),
+ ),
+ height: Math.max(
+ 1,
+ Math.round(viewport?.height || window.innerHeight || 0),
+ ),
+ };
}
function syncViewportCssVars() {
const rootStyle = document.documentElement?.style;
if (!rootStyle) return;
- const viewport = window.visualViewport;
- const width = Math.max(
- 1,
- Math.round(viewport?.width || window.innerWidth || 0),
- );
- const height = Math.max(
- 1,
- Math.round(viewport?.height || window.innerHeight || 0),
- );
+ const { width, height } = getViewportMetrics();
rootStyle.setProperty("--bme-viewport-width", `${width}px`);
rootStyle.setProperty("--bme-viewport-height", `${height}px`);
}
+function getFabFallbackSize() {
+ return _isMobile() ? 54 : 46;
+}
+
+function getFabSize(fab = _fabEl) {
+ if (fab) {
+ const rect = fab.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) {
+ return {
+ width: rect.width,
+ height: rect.height,
+ };
+ }
+ }
+
+ const fallback = getFabFallbackSize();
+ return {
+ width: fallback,
+ height: fallback,
+ };
+}
+
+function getDefaultFabPosition(fab = _fabEl) {
+ const { width: viewportWidth, height: viewportHeight } = getViewportMetrics();
+ const { width, height } = getFabSize(fab);
+ const sideGap = _isMobile() ? 14 : 16;
+ const bottomGap = _isMobile() ? 96 : 80;
+
+ return {
+ x: Math.max(sideGap, viewportWidth - width - sideGap),
+ y: Math.max(sideGap, viewportHeight - height - bottomGap),
+ };
+}
+
+function clampFabPosition(position = {}, fab = _fabEl) {
+ const { width: viewportWidth, height: viewportHeight } = getViewportMetrics();
+ const { width, height } = getFabSize(fab);
+ const margin = _isMobile() ? 10 : 8;
+ const maxX = Math.max(margin, viewportWidth - width - margin);
+ const maxY = Math.max(margin, viewportHeight - height - margin);
+ const x = Number.isFinite(position?.x) ? position.x : maxX;
+ const y = Number.isFinite(position?.y) ? position.y : maxY;
+
+ return {
+ x: Math.min(Math.max(margin, Math.round(x)), Math.round(maxX)),
+ y: Math.min(Math.max(margin, Math.round(y)), Math.round(maxY)),
+ };
+}
+
+function applyFabPosition(position = {}, fab = _fabEl) {
+ if (!fab) return;
+ const clamped = clampFabPosition(position, fab);
+ fab.style.left = `${clamped.x}px`;
+ fab.style.top = `${clamped.y}px`;
+ fab.style.right = "auto";
+ fab.style.bottom = "auto";
+}
+
+function syncFabPosition() {
+ if (!_fabEl) return;
+
+ ensureFabMountedAtRoot();
+ const mode = _fabEl.dataset.positionMode || "default";
+ if (mode === "saved") {
+ const currentX = Number.parseFloat(_fabEl.style.left);
+ const currentY = Number.parseFloat(_fabEl.style.top);
+ const fallback =
+ _loadFabPosition() ||
+ getDefaultFabPosition(_fabEl);
+ const next = clampFabPosition(
+ {
+ x: Number.isFinite(currentX) ? currentX : fallback.x,
+ y: Number.isFinite(currentY) ? currentY : fallback.y,
+ },
+ _fabEl,
+ );
+ applyFabPosition(next, _fabEl);
+ _saveFabPosition(next.x, next.y);
+ return;
+ }
+
+ applyFabPosition(getDefaultFabPosition(_fabEl), _fabEl);
+}
+
function bindViewportSync() {
if (viewportSyncBound) return;
viewportSyncBound = true;
- const update = () => syncViewportCssVars();
+ const update = () => {
+ syncViewportCssVars();
+ syncFabPosition();
+ };
window.addEventListener("resize", update);
window.addEventListener("orientationchange", update);
window.visualViewport?.addEventListener("resize", update);
@@ -311,7 +414,13 @@ function _getFabVisible() {
function _setFabVisible(visible) {
try { localStorage.setItem(FAB_VISIBLE_KEY, String(visible)); } catch {}
- if (_fabEl) _fabEl.style.display = visible ? "flex" : "none";
+ if (_fabEl) {
+ ensureFabMountedAtRoot();
+ _fabEl.style.display = visible ? "flex" : "none";
+ if (visible) {
+ syncFabPosition();
+ }
+ }
const btn = panelEl?.querySelector("#bme-fab-toggle-btn");
if (btn) btn.setAttribute("data-active", String(visible));
}
@@ -327,7 +436,13 @@ function _bindFabToggle() {
}
function _initFloatingBall() {
- if (document.getElementById("bme-floating-ball")) return;
+ const existing = document.getElementById("bme-floating-ball");
+ if (existing) {
+ _fabEl = existing;
+ ensureFabMountedAtRoot();
+ syncFabPosition();
+ return;
+ }
const fab = document.createElement("div");
fab.id = "bme-floating-ball";
@@ -336,8 +451,8 @@ function _initFloatingBall() {
BME 记忆图谱
`;
- document.body.appendChild(fab);
_fabEl = fab;
+ ensureFabMountedAtRoot();
// 应用可见性
if (!_getFabVisible()) fab.style.display = "none";
@@ -345,13 +460,11 @@ function _initFloatingBall() {
// 恢复位置
const saved = _loadFabPosition();
if (saved) {
- fab.style.left = `${saved.x}px`;
- fab.style.top = `${saved.y}px`;
- fab.style.right = "auto";
- fab.style.bottom = "auto";
+ fab.dataset.positionMode = "saved";
+ applyFabPosition(saved, fab);
} else {
- fab.style.right = "16px";
- fab.style.bottom = "80px";
+ fab.dataset.positionMode = "default";
+ syncFabPosition();
}
// 拖拽 + 点击逻辑
@@ -383,17 +496,13 @@ function _initFloatingBall() {
if (!hasMoved && Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return;
hasMoved = true;
- let newX = fabStartX + dx;
- let newY = fabStartY + dy;
- // 限制在视口内
- const size = 46;
- newX = Math.max(0, Math.min(window.innerWidth - size, newX));
- newY = Math.max(0, Math.min(window.innerHeight - size, newY));
-
- fab.style.left = `${newX}px`;
- fab.style.top = `${newY}px`;
- fab.style.right = "auto";
- fab.style.bottom = "auto";
+ applyFabPosition(
+ {
+ x: fabStartX + dx,
+ y: fabStartY + dy,
+ },
+ fab,
+ );
}
function onPointerUp(e) {
@@ -403,7 +512,11 @@ function _initFloatingBall() {
if (hasMoved) {
// 拖拽结束 → 保存位置
- _saveFabPosition(parseInt(fab.style.left), parseInt(fab.style.top));
+ fab.dataset.positionMode = "saved";
+ _saveFabPosition(
+ Number.parseInt(fab.style.left, 10),
+ Number.parseInt(fab.style.top, 10),
+ );
return;
}
diff --git a/style.css b/style.css
index d8186c4..a857d79 100644
--- a/style.css
+++ b/style.css
@@ -2628,3 +2628,22 @@
opacity: 1;
}
+@media (max-width: 768px) {
+ #bme-floating-ball {
+ width: 54px;
+ height: 54px;
+ border-width: 2px;
+ box-shadow:
+ 0 8px 24px rgba(0, 0, 0, 0.42),
+ 0 0 0 1px rgba(255, 255, 255, 0.06) inset;
+ }
+
+ #bme-floating-ball .bme-fab-icon {
+ font-size: 21px;
+ }
+
+ #bme-floating-ball .bme-fab-tooltip {
+ display: none;
+ }
+}
+