// ======================================== // 主题功能模块 // ======================================== import { $, debounce, throttle } from './utils.js'; import { config } from './config.js'; import { getAllButtons } from './buttons.js'; import { initTabBarsMarginUnified, cleanupTopbarMerge } from './topbarMerge.js'; import { initMiddleClickCollapse, cleanupMiddleClickCollapse } from './middleClickCollapse.js'; import { initViewSelect, cleanupViewSelect } from './viewSelect.js'; import { initMindmapDrag } from './mindmapDrag.js'; import { initPlatformDetection, cleanupPlatformDetection } from './platform.js'; import { initSlashMenuNavigation } from './slashMenuNav.js'; import { initSuperBlockResizer } from './superBlockResizer.js'; import { initMobileAndPlatformFeatures, cleanupMobileMenu } from './mobileMenu.js'; // 主题相关变量 let featureButtonsActive = new Set(); let domCache = new Map(); let menuListeners = new WeakMap(); // 获取缓存元素 const getCachedElement = (selector) => { if (!domCache.has(selector)) { domCache.set(selector, document.querySelector(selector)); } return domCache.get(selector); }; // 获取配置项 const getItem = config.get; // 应用记住的主题样式 export const applyRememberedThemeStyle = async (skipFeatures = false) => { // 只应用当前主题模式的主题,而不是同时应用light和dark const currentThemeMode = window.theme.themeMode; // 处理主题组 const themeButtons = getAllButtons().filter(btn => btn.type === 'theme' && btn.group === currentThemeMode); const [rememberedButton, defaultButton] = themeButtons.reduce((acc, btn) => { if (config.get(btn.id) === "1") acc[0] = btn; if (btn.styleId === (currentThemeMode === 'light' ? 'Sv-theme-color-light' : 'Sv-theme-color-dark')) acc[1] = btn; return acc; }, [null, null]); // 先清理所有主题的savor-theme属性 getAllButtons().filter(btn => btn.type === 'theme').forEach(btn => btn.onDisable?.()); // 启用记住的主题或默认主题 const buttonToEnable = rememberedButton || defaultButton; buttonToEnable?.onEnable?.(); // 处理彩色标题功能(特定主题使用) const currentTheme = buttonToEnable?.styleId || (currentThemeMode === 'light' ? 'Sv-theme-color-light' : 'Sv-theme-color-dark'); updateColorfulHeading(currentTheme); // 更新按钮状态 const savorToolbar = document.getElementById("savorToolbar"); savorToolbar?.querySelectorAll('.b3-menu__item').forEach(btn => btn.classList.remove('button_on')); // 为记住的主题按钮添加激活状态 document.getElementById(buttonToEnable?.id)?.classList.add('button_on'); if (!skipFeatures) { await applyRememberedFeatures(); } }; // 应用记住的功能 const applyRememberedFeatures = async () => { for (const btn of getAllButtons()) { if (btn.type === 'feature' && config.get(btn.attrName) === "1") { featureButtonsActive.add(btn.id); const button = document.getElementById(btn.id); button?.classList.add("button_on"); // 现在所有功能都通过属性控制,不需要加载 CSS btn.onEnable?.(); } } // 确保超级块宽度调节功能已初始化 if (typeof window.superBlockResizer?.start === 'function') { window.superBlockResizer.start(); } else if (typeof window.initSuperBlockResizer === 'function') { window.initSuperBlockResizer(); } }; // 主题组工具函数 const getThemeGroupButtons = (group) => getAllButtons().filter(btn => btn.type === 'theme' && btn.group === group); const forEachThemeInGroup = (group, fn) => getThemeGroupButtons(group).forEach(fn); const disableThemeGroup = (group) => forEachThemeInGroup(group, b => b.onDisable?.()); const clearRememberedThemeGroup = (group) => forEachThemeInGroup(group, b => config.set(b.id, "0")); const unmarkThemeGroupButtons = (group) => forEachThemeInGroup(group, b => document.getElementById(b.id)?.classList.remove('button_on')); const markThemeButton = (id) => document.getElementById(id)?.classList.add('button_on'); // 应用主题组 export const applyThemeForGroup = async (group, btn) => { window.theme.applyThemeTransition(async () => { disableThemeGroup(group); btn.onEnable?.(); updateColorfulHeading(btn.styleId); await applyRememberedFeatures(); setTimeout(() => { window.statusObserver?.updatePosition?.(); }, 100); }); clearRememberedThemeGroup(group); config.set(btn.id, "1"); }; // 渲染所有按钮 export const renderAllButtons = (targetToolbar = null) => { const savorToolbar = targetToolbar || document.getElementById("savorToolbar"); if (!savorToolbar) return; savorToolbar.innerHTML = ""; const fragment = document.createDocumentFragment(); // 先配色后功能 const themeMode = window.theme.themeMode; // 根据当前模式显示对应组的主题按钮 const [themeButtons, featureButtons] = getAllButtons().reduce((acc, btn) => { if (btn.type === 'theme' && btn.group === themeMode) acc[0].push(btn); else if (btn.type === 'feature') acc[1].push(btn); return acc; }, [[], []]); const buttons = [...themeButtons, ...featureButtons]; buttons.forEach(btn => { const button = document.createElement("button"); button.id = btn.id; button.className = "b3-menu__item savor-button"; button.setAttribute("aria-label", btn.label); // 根据按钮类型使用不同的 SVG 处理方式 if (btn.type === 'theme') { // 主题按钮使用完整的 SVG 结构 button.innerHTML = `${btn.svg}${btn.label}`; } else { // 功能按钮使用路径数据 button.innerHTML = `${btn.label}`; } button.setAttribute("data-type", btn.type); // 状态高亮 const isActivated = (btn.type === 'theme' && config.get(btn.id) === "1") || (btn.type === 'feature' && getItem(btn.attrName) === "1"); if (isActivated) { button.classList.add("button_on"); } // 点击逻辑 button.addEventListener("click", async () => { if (btn.type === 'theme') { const currentGroup = btn.group; if (currentGroup) { unmarkThemeGroupButtons(currentGroup); } button.classList.add('button_on'); await applyThemeForGroup(currentGroup, btn); } else if (btn.type === 'feature') { const isActive = button.classList.contains("button_on"); if (isActive) { button.classList.remove("button_on"); document.getElementById(btn.styleId)?.remove(); btn.onDisable?.(); config.set(btn.attrName, "0"); } else { button.classList.add("button_on"); // 现在所有功能都通过属性控制,不需要加载 CSS btn.onEnable?.(); config.set(btn.attrName, "1"); } } }); fragment.appendChild(button); }); savorToolbar.appendChild(fragment); }; // 工具栏和观察器管理 const shouldShowSavorToolbar = () => { const mode = window.theme.themeMode; return document.documentElement.getAttribute(`data-${mode}-theme`) === "Savor"; }; const handleMenuClick = (e) => { const menuItem = e.target.closest(".b3-menu__item"); if (!menuItem) return; const buttonText = menuItem.textContent.trim(); const { themeLight, themeDark, themeOS } = window.siyuan.languages; // 简化条件判断 if (![themeLight, themeDark, themeOS].includes(buttonText)) return; let targetMode = buttonText === themeOS ? (window.matchMedia("(prefers-color-scheme: light)").matches ? themeLight : themeDark) : buttonText; const currentMode = window.siyuan.config.appearance.mode === 0 ? themeLight : themeDark; if (targetMode === currentMode) return; const targetTheme = targetMode === themeLight ? window.siyuan.config.appearance.themeLight : window.siyuan.config.appearance.themeDark; if (targetTheme !== "Savor") return; window.theme.applyThemeTransition(() => {}); }; export const toggleMenuListener = (commonMenu, add = true) => { if (add && !menuListeners.has(commonMenu)) { menuListeners.set(commonMenu, handleMenuClick); commonMenu.addEventListener("click", handleMenuClick, true); } else if (!add && menuListeners.has(commonMenu)) { const listener = menuListeners.get(commonMenu); commonMenu.removeEventListener("click", listener, true); menuListeners.delete(commonMenu); } }; // 通过 CSS 控制 savorToolbar 的可见性 const ensureSavorToolbarCSS = () => { const id = "savor-toolbar-visibility"; let st = document.getElementById(id); if (!st) { st = document.createElement("style"); st.id = id; st.textContent = ` #commonMenu[data-name="barmode"] #savorToolbar { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; } `; document.head.appendChild(st); } }; // 初始化工具栏 export const initSavorToolbar = () => { ensureSavorToolbarCSS(); // 移动端不创建桌面工具栏 if (window.SavorPlatform?.isMobile?.() || document.getElementById("savorToolbar")) return; const commonMenu = document.getElementById("commonMenu"); if (!commonMenu || !shouldShowSavorToolbar()) return; const savorToolbar = document.createElement("div"); savorToolbar.id = "savorToolbar"; // 简化:始终插入为 #commonMenu 的第一个子元素 commonMenu.insertBefore(savorToolbar, commonMenu.firstChild); renderAllButtons(); requestAnimationFrame(() => applyRememberedThemeStyle()); }; // 底栏悬浮 export const initStatusPosition = () => { let lastOffset = null; const updatePosition = () => { const status = Savor$("#status"); if (!status) return; const dockr = Savor$(".layout__dockr"), dockVertical = Savor$(".dock--vertical"); const dockrWidth = dockr?.offsetWidth || 0; const isFloating = dockr?.classList.contains("layout--float"); const dockVerticalWidth = (!dockVertical || dockVertical.classList.contains("fn__none")) ? 0 : 26; // 简化offset计算 const baseOffset = dockVerticalWidth ? dockVerticalWidth + 16 : 9; const offset = (!dockrWidth || isFloating) ? baseOffset : dockrWidth + baseOffset; const layoutCenter = document.querySelector('.layout__center'); status.style.maxWidth = layoutCenter ? `${layoutCenter.offsetWidth - 8}px` : ''; if (lastOffset !== offset) status.style.transform = `translateX(-${offset}px)`; lastOffset = offset; }; const observer = new ResizeObserver(throttle(updatePosition, 16)); [".layout__dockr", ".dock--vertical", ".layout__center"].forEach(sel => { const el = Savor$(sel); el && observer.observe(el); }); window.statusObserver = observer; window.statusObserver.updatePosition = updatePosition; updatePosition(); }; // 初始化主题观察器 export const initThemeObserver = () => { let previousThemeMode = window.theme.themeMode; let previousThemeName = shouldShowSavorToolbar() ? "Savor" : null; const themeObserver = new MutationObserver(debounce(() => { const newThemeMode = window.siyuan.config.appearance.mode === 0 ? "light" : "dark"; const html = document.documentElement; const newThemeName = html.getAttribute(`data-${newThemeMode}-theme`); if (previousThemeMode === newThemeMode && previousThemeName === newThemeName) return; const isSavorToSavor = previousThemeName === "Savor" && newThemeName === "Savor"; const commonMenu = getCachedElement("#commonMenu"); const existingSavorToolbar = document.getElementById("savorToolbar"); if (commonMenu?.getAttribute("data-name") === "barmode") { if (shouldShowSavorToolbar()) { if (!existingSavorToolbar) { initSavorToolbar(); } else { window.theme.applyThemeTransition(async () => { renderAllButtons(); await applyRememberedThemeStyle(isSavorToSavor); }); } } else if (existingSavorToolbar) { // 不再移除,交由 CSS 控制显示 } // 确保超级块调节功能在主题切换后正常工作 try { if (typeof window.superBlockResizer?.refresh === 'function') { setTimeout(() => window.superBlockResizer.refresh(), 100); } } catch (e) { // 超级块调节功能刷新出错: e } } previousThemeMode = newThemeMode; previousThemeName = newThemeName; }, 16)); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme-mode", "data-light-theme", "data-dark-theme", "class", "data-mode"] }); }; // topBarPlugin 菜单调整 export const initTopBarPluginMenuObserver = () => { window.topBarPluginMenuObserver = new MutationObserver(() => { const commonMenu = document.getElementById("commonMenu"); if (!commonMenu || commonMenu.getAttribute("data-name") !== "topBarPlugin") return; commonMenu.querySelectorAll(".b3-menu__submenu").forEach(submenu => { const parentItem = submenu.parentElement; const buttons = Array.from(submenu.querySelectorAll(".b3-menu__item")); buttons.forEach(btn => { btn.classList.add("submenu-inline"); const iconSmall = parentItem.querySelector(".b3-menu__icon--small"); // 根据图标位置决定按钮插入位置 if (iconSmall) { const insertRef = iconSmall.nextSibling || parentItem; (insertRef === parentItem ? insertRef.appendChild : insertRef.parentElement.insertBefore) .call(insertRef === parentItem ? insertRef : insertRef.parentElement, btn, insertRef); } else { parentItem.appendChild(btn); } }); // 如果子菜单为空则移除 if (!submenu.querySelector(".b3-menu__item")) submenu.remove(); }); }); const _topBarObserveTarget = document.getElementById("commonMenu") || document.body; window.topBarPluginMenuObserver.observe(_topBarObserveTarget, { childList: true, subtree: true }); }; // 主题清理函数 export const destroyTheme = () => { // 清理功能按钮和全局变量 window.allButtons?.forEach(btn => btn.type === 'feature' && Savor$(`#${btn.id}`)?.remove()); window.featureButtonsActive?.clear(); Object.assign(window, { tabBarsMarginInitialized: false, updateTabBarsMargin: null, updateTabBarsMarginLeft: null }); // 断开观察器 [window.statusObserver, window.topBarPluginMenuObserver].forEach(obs => obs?.disconnect()); // 清理DOM和样式 Savor$$('[id^="Sv-theme-color"], #savorToolbar, #savor-toolbar-visibility').forEach(el => el.remove()); Savor$('#status')?.style.setProperty('transform', ''); Savor$('#status')?.style.setProperty('max-width', ''); // 清理缓存和属性 window.domCache?.clear(); Savor$('#commonMenu') && window.toggleMenuListener?.(Savor$('#commonMenu'), false); document.documentElement.removeAttribute('savor-theme'); document.documentElement.removeAttribute('savor-tabbar'); // 统一清理所有功能模块 const cleanupFunctions = [ 'cleanupTopbarMerge', 'cleanupTabbarVertical', 'cleanupBulletThreading', 'cleanupTypewriterMode', 'cleanupSidebarMemo', 'cleanupMiddleClickCollapse', 'cleanupPlatformDetection', 'cleanupMobileMenu', 'disableListPreview' ]; cleanupFunctions.forEach(funcName => { if (typeof window[funcName] === 'function') { try { window[funcName](); } catch (e) { // ${funcName}执行失败: e } } }); // 清理视图选择功能(先断开所有监听器) try { if (typeof cleanupViewSelect === 'function') { cleanupViewSelect(); } } catch (e) { // [Savor] 清理视图选择功能时出错: ${e.message} } // 清理标签页 DOM,恢复原始结构(在断开监听器后执行) setTimeout(() => { try { document.querySelectorAll('.protyle-wysiwyg [data-type="NodeList"]').forEach(listElement => { if (listElement._convertedToTab && listElement._originalHTML) { listElement.innerHTML = listElement._originalHTML; delete listElement._originalHTML; delete listElement._convertedToTab; listElement.removeAttribute('custom-f'); } }); } catch (e) { // [Savor] 清理标签页 DOM 时出错: ${e.message} } }, 100); // 重新初始化超级块宽度调节功能 try { // 停止功能 if (typeof window.superBlockResizer?.stop === 'function') { window.superBlockResizer.stop(); } // 移除样式 const styleElement = document.getElementById('sb-resizer-styles'); if (styleElement) { styleElement.remove(); } // 重新初始化功能 window.superBlockResizer = null; if (typeof window.initSuperBlockResizer === 'function') { window.initSuperBlockResizer(); } } catch (e) { // [Savor] 重新初始化超级块宽度调节功能时出错: ${e.message} } // 清理导图拖拽功能 try { // 直接调用导图模块的清理函数 if (typeof window.cleanupMindmapDrag === 'function') { window.cleanupMindmapDrag(); } } catch (e) { // [Savor] 清理导图拖拽功能时出错: ${e.message} } // 清理彩色标题样式元素 const colorfulHeadingStyle = document.getElementById("snippet-SvcolorfulHeading"); if (colorfulHeadingStyle) { colorfulHeadingStyle.remove(); } // 额外清理可能存在的定时器 if (window._tabBarUpdateTimer) { clearTimeout(window._tabBarUpdateTimer); window._tabBarUpdateTimer = null; } // 清理可能存在的其他全局变量 window._tabBarsResizeObserver = null; window.updateTabBarsMargin = null; window.updateTabBarsMarginLeft = null; }; // 初始化主题功能 export const initTheme = () => { // [Savor] 初始化主题功能 // 将函数添加到全局作用域 Object.assign(window, { applyRememberedThemeStyle, applyThemeForGroup, renderAllButtons, initSavorToolbar, initStatusPosition, initThemeObserver, initTopBarPluginMenuObserver, toggleMenuListener, destroyTheme }); // 添加window.theme对象 if (!window.theme) { window.theme = { get config() { return config.data; }, get themeMode() { return window.siyuan?.config?.appearance?.mode === 0 ? 'light' : 'dark'; }, applyThemeTransition: (callback) => { const status = Savor$('#status'); const currentTransform = status?.style.transform; // 简化回调执行逻辑 const executeCallback = () => { callback?.(); if (status && currentTransform) { setTimeout(() => status.style.transform = currentTransform, 50); } }; (document.startViewTransition && document.startViewTransition(executeCallback)) || executeCallback(); }, findEditableParent: (node) => { const editableSelectors = ['[contenteditable="true"]', '.protyle-wysiwyg']; return editableSelectors.reduce((found, selector) => found || node.closest(selector), null); }, createElementEx: (refElement, tag, id = null, mode = 'append') => { if (!refElement || !tag) { // [Savor] 参考元素或标签名不存在 return null; } const el = document.createElement(tag); if (id) el.id = id; try { const insertModes = { append: () => refElement.appendChild(el), prepend: () => refElement.insertBefore(el, refElement.firstChild), before: () => refElement.parentElement.insertBefore(el, refElement) }; insertModes[mode]?.(); return el; } catch (error) { // [Savor] 创建元素失败: ${error.message} return null; } }, BodyEventRunFun: (eventStr, fun, accurate = 100, delay = 0, frequency = 1, frequencydelay = 16) => { let isMove = false; let _e = null; window.theme.EventUtil.on(document.body, eventStr, (e) => { isMove = true; _e = e; }); setInterval(() => { if (!isMove) return; isMove = false; setTimeout(() => { fun(_e); if (frequency === 1) return; const minDelay = Math.max(16, frequencydelay); for (let i = 1; i < frequency; i++) { setTimeout(() => fun(_e), minDelay * i); } }, delay); }, accurate); }, EventUtil: { on: (element, type, handler) => element?.addEventListener?.(type, handler, false), off: (element, type, handler) => element?.removeEventListener?.(type, handler, false) }, findAncestor: (element, fn, maxDepth = 50) => { let depth = 0, parent = element?.parentElement; while (parent && depth < maxDepth) { if (fn(parent)) return parent; parent = parent.parentElement; depth++; } return null; }, isSiyuanFloatingWindow: (element) => window.theme.findAncestor(element, v => v.getAttribute("data-oid") != null), setBlockFold: (id, fold) => { if (!id || (window._lastFoldedId === id && window._lastFoldedState === fold)) return; window._lastFoldedId = id; window._lastFoldedState = fold; 设置思源块属性(id, { fold }); }, // 顶栏合并功能相关函数 initTabBarsMarginUnified, cleanupTopbarMerge }; } // 添加平台检测功能到window.theme对象 window.theme.initPlatformDetection = initPlatformDetection; window.theme.cleanupPlatformDetection = cleanupPlatformDetection; // 添加各功能模块的清理函数到window.theme对象 Object.assign(window.theme, { cleanupTabbarVertical: window.cleanupTabbarVertical, cleanupBulletThreading: window.cleanupBulletThreading, cleanupTypewriterMode: window.cleanupTypewriterMode, cleanupSidebarMemo: window.cleanupSidebarMemo, cleanupMiddleClickCollapse: cleanupMiddleClickCollapse }); // 初始化topBarPlugin菜单观察器 initTopBarPluginMenuObserver(); // 初始化鼠标中键折叠/展开功能 initMiddleClickCollapse(); // 初始化视图选择UI功能 initViewSelect(); // 初始化导图拖拽功能 initMindmapDrag(); // 初始化平台检测功能 initPlatformDetection(); // 初始化斜杠菜单左右键导航功能 initSlashMenuNavigation(); // 初始化超级块宽度调节功能 initSuperBlockResizer(); }; // 新增:统一处理彩色标题样式的工具函数,消除重复逻辑 const updateColorfulHeading = (styleId) => { const colorfulThemes = ["Sv-theme-color-sugar", "Sv-theme-color-flower"]; if (colorfulThemes.includes(styleId)) { // 启用彩色标题样式(仅限亮色主题) let element = document.getElementById("snippet-SvcolorfulHeading"); if (!element) { element = document.createElement("style"); element.id = "snippet-SvcolorfulHeading"; element.innerHTML = ` :root[data-theme-mode="light"] { --Sv-h1: var(--h1-list-graphic); --Sv-h2: #8a7da0; --Sv-h3: var(--h3-list-graphic); --Sv-h4: var(--h4-list-graphic); --Sv-h5: #b6a277; --Sv-h6: var(--h6-list-graphic); }`; document.head.appendChild(element); } } else { // 移除彩色标题样式 document.getElementById("snippet-SvcolorfulHeading")?.remove(); } };