diff --git a/assets/js/core/event-bus.ts b/assets/js/core/event-bus.ts index 9ff0b877..9480ca66 100644 --- a/assets/js/core/event-bus.ts +++ b/assets/js/core/event-bus.ts @@ -22,14 +22,14 @@ export type Handler = T extends void /** Typed event bus — wraps DOM CustomEvents with type-safe emit/on/off. */ export class TypedEventBus { - private target = document + #target = document on(event: K, handler: Handler): void { - this.target.addEventListener(event as string, handler as EventListener) + this.#target.addEventListener(event as string, handler as EventListener) } off(event: K, handler: Handler): void { - this.target.removeEventListener(event as string, handler as EventListener) + this.#target.removeEventListener(event as string, handler as EventListener) } emit( @@ -37,7 +37,7 @@ export class TypedEventBus { ...args: FixItEventMap[K] extends void ? [] : [FixItEventMap[K]] ): void { const detail = args[0] - this.target.dispatchEvent( + this.#target.dispatchEvent( detail !== undefined ? new CustomEvent(event as string, { detail }) : new CustomEvent(event as string), diff --git a/assets/js/modules/content.ts b/assets/js/modules/content.ts index d86cd311..e2ff1af0 100644 --- a/assets/js/modules/content.ts +++ b/assets/js/modules/content.ts @@ -16,10 +16,13 @@ const copyText = createCopyText() * - Re-initialize components after encrypted content is decrypted. */ export class ContentModule implements ContentService { - constructor( - private readonly core: CoreService, - private readonly code: CodeService, - ) {} + #core: CoreService + #code: CodeService + + constructor(core: CoreService, code: CodeService) { + this.#core = core + this.#code = code + } /** Fetch and inline SVG icons referenced by `data-svg-src` attributes. */ initSVGIcon() { @@ -131,7 +134,7 @@ export class ContentModule implements ContentService { return const footnoteMap = new Map() $footnoteRefs.forEach(($ref) => { - if (this.core.config.tooltip) { + if (this.#core.config.tooltip) { const $link = $ref.querySelector('a.footnote-ref') if ($link) { $link.addEventListener('click', (e) => { @@ -153,7 +156,7 @@ export class ContentModule implements ContentService { if ($ref.hasAttribute('title')) return $ref.setAttribute('title', $content.textContent!.trim()) - if (this.core.config.tooltip) { + if (this.#core.config.tooltip) { CellTooltip.getOrCreateInstance($ref) } }) @@ -161,7 +164,7 @@ export class ContentModule implements ContentService { /** Initialize CellTooltip on action buttons, copy buttons, and footnotes. */ initTooltip() { - if (!this.core.config.tooltip) + if (!this.#core.config.tooltip) return CellTooltip.initAll('li[data-task] > span[title]', { placement: 'right' }) CellTooltip.initAll('.action-btn[title]', { placement: 'bottom' }) @@ -177,9 +180,9 @@ export class ContentModule implements ContentService { */ initContent(target: Element | Document = document) { this.initDetails(target) - this.code.initCodeWrapper() - this.code.initCodeTabs() - this.code.initDiagramCopyBtn() + this.#code.initCodeWrapper() + this.#code.initCodeTabs() + this.#code.initDiagramCopyBtn() this.initTooltip() this.initLinkGuardDialog(target) } diff --git a/assets/js/modules/core.ts b/assets/js/modules/core.ts index 7b159361..14d48fd4 100644 --- a/assets/js/modules/core.ts +++ b/assets/js/modules/core.ts @@ -16,8 +16,8 @@ export class CoreModule implements CoreService { themeMode: string isDark: boolean - private activeMaskOverlay: string | null = null - private readonly maskOverlays = new Map() + #activeMaskOverlay: string | null = null + #maskOverlays = new Map() constructor() { this.config = window.config @@ -30,46 +30,46 @@ export class CoreModule implements CoreService { /** Register a named mask overlay with open/close/isActive handlers. */ registerMaskOverlay(name: string, handlers: MaskOverlayHandler) { - this.maskOverlays.set(name, handlers) + this.#maskOverlays.set(name, handlers) } /** Toggle the mask element's blur class based on active overlay state. */ syncMaskState() { - document.getElementById('mask')?.classList.toggle('is-blur', Boolean(this.activeMaskOverlay)) + document.getElementById('mask')?.classList.toggle('is-blur', Boolean(this.#activeMaskOverlay)) } /** Open a named mask overlay, closing any previously active one. */ openMaskOverlay(name: string) { - const overlay = this.maskOverlays.get(name) + const overlay = this.#maskOverlays.get(name) if (!overlay) return - if (this.activeMaskOverlay && this.activeMaskOverlay !== name) { - this.closeMaskOverlay(this.activeMaskOverlay, true) + if (this.#activeMaskOverlay && this.#activeMaskOverlay !== name) { + this.closeMaskOverlay(this.#activeMaskOverlay, true) } overlay.onOpen?.() - this.activeMaskOverlay = name + this.#activeMaskOverlay = name this.syncMaskState() } /** Close a named mask overlay and optionally skip mask state sync. */ closeMaskOverlay(name: string, skipSync = false) { - const overlay = this.maskOverlays.get(name) + const overlay = this.#maskOverlays.get(name) if (!overlay) return overlay.onClose?.() - if (this.activeMaskOverlay === name) { - this.activeMaskOverlay = null + if (this.#activeMaskOverlay === name) { + this.#activeMaskOverlay = null } !skipSync && this.syncMaskState() } /** Toggle a named mask overlay open/closed. */ toggleMaskOverlay(name: string) { - const overlay = this.maskOverlays.get(name) + const overlay = this.#maskOverlays.get(name) if (!overlay) return - const isActive = overlay.isActive?.() ?? this.activeMaskOverlay === name - if (this.activeMaskOverlay === name && isActive) { + const isActive = overlay.isActive?.() ?? this.#activeMaskOverlay === name + if (this.#activeMaskOverlay === name && isActive) { this.closeMaskOverlay(name) return } @@ -78,10 +78,10 @@ export class CoreModule implements CoreService { /** Close whichever mask overlay is currently active. */ closeActiveMaskOverlay() { - if (!this.activeMaskOverlay) { + if (!this.#activeMaskOverlay) { this.syncMaskState() return } - this.closeMaskOverlay(this.activeMaskOverlay) + this.closeMaskOverlay(this.#activeMaskOverlay) } } diff --git a/assets/js/modules/encryption.ts b/assets/js/modules/encryption.ts index ac8a9b60..55c989ab 100644 --- a/assets/js/modules/encryption.ts +++ b/assets/js/modules/encryption.ts @@ -7,14 +7,18 @@ import type { CoreService, EncryptionService } from '../core/tokens' * - Initialize FixItDecryptor for full-page and shortcode-scoped decryption. */ export class EncryptionModule implements EncryptionService { - constructor(private readonly core: CoreService) {} + #core: CoreService + + constructor(core: CoreService) { + this.#core = core + } /** Initialize FixItDecryptor with encryption config. */ setup() { - if (!this.core.config.encryption || !window.FixItDecryptor) + if (!this.#core.config.encryption || !window.FixItDecryptor) return const decryptor = new window.FixItDecryptor() - decryptor.init(this.core.config.encryption) + decryptor.init(this.#core.config.encryption) } } diff --git a/assets/js/modules/events.ts b/assets/js/modules/events.ts index 7f219827..2f22694a 100644 --- a/assets/js/modules/events.ts +++ b/assets/js/modules/events.ts @@ -15,11 +15,13 @@ export class EventsModule implements EventsService { #resizeTimeout: number | null = null #newScrollTop = 0 #oldScrollTop = 0 + #core: CoreService + #code: CodeService - constructor( - private readonly core: CoreService, - private readonly code: CodeService, - ) {} + constructor(core: CoreService, code: CodeService) { + this.#core = core + this.#code = code + } /** Bind scroll listener: auto-hide headers, reading progress, back-to-top, and TOC sync. */ onScroll() { @@ -40,7 +42,7 @@ export class EventsModule implements EventsService { this.#newScrollTop = getScrollTop() const scroll = this.#newScrollTop - this.#oldScrollTop if (Math.abs(scroll) > ACCURACY) { - this.core.closeActiveMaskOverlay() + this.#core.closeActiveMaskOverlay() const isScrollingDown = scroll > 0 $autoHeaders.forEach(($header) => { if (isScrollingDown) { @@ -97,7 +99,7 @@ export class EventsModule implements EventsService { const _isMobile = isMobile() if (_isMobile !== resizeBefore) { - this.core.closeActiveMaskOverlay() + this.#core.closeActiveMaskOverlay() resizeBefore = _isMobile } }, 100) @@ -110,7 +112,7 @@ export class EventsModule implements EventsService { document.getElementById('mask')!.addEventListener('click', (e) => { if (!(e.target as HTMLElement).classList.contains('is-blur')) return - this.core.closeActiveMaskOverlay() + this.#core.closeActiveMaskOverlay() }, false) } @@ -118,7 +120,7 @@ export class EventsModule implements EventsService { initPrint() { window.addEventListener('beforeprint', () => { const $content = document.getElementById('content')! - const printConfig = this.core.config.print || {} + const printConfig = this.#core.config.print || {} if (printConfig.expandAdmonition) { $content.querySelectorAll('.admonition').forEach(($el: Element) => $el.classList.add('open')) @@ -156,7 +158,7 @@ export class EventsModule implements EventsService { }, false) window.addEventListener('afterprint', () => { - this.code.initCodeTabs() + this.#code.initCodeTabs() }, false) } diff --git a/assets/js/modules/menu.ts b/assets/js/modules/menu.ts index 64a447ba..02014b13 100644 --- a/assets/js/modules/menu.ts +++ b/assets/js/modules/menu.ts @@ -9,7 +9,11 @@ import type { CoreService, MenuService } from '../core/tokens' * - Sync menu state with mask overlay. */ export class MenuModule implements MenuService { - constructor(private readonly core: CoreService) {} + #core: CoreService + + constructor(core: CoreService) { + this.#core = core + } /** Set min-width on desktop sub-menus to match parent item width. */ initDesktop() { @@ -24,7 +28,7 @@ export class MenuModule implements MenuService { const $menuMobile = document.getElementById('menu-mobile') if (!$menuToggleMobile || !$menuMobile) return - this.core.registerMaskOverlay('menu-mobile', { + this.#core.registerMaskOverlay('menu-mobile', { isActive: () => $menuMobile.classList.contains('active'), onOpen: () => { $menuToggleMobile.classList.add('active') @@ -38,7 +42,7 @@ export class MenuModule implements MenuService { }, }) $menuToggleMobile.addEventListener('click', () => { - this.core.toggleMaskOverlay('menu-mobile') + this.#core.toggleMaskOverlay('menu-mobile') }, false) // add nested menu toggler document.querySelectorAll('.menu-item>.nested-item').forEach(($nestedItem) => { diff --git a/assets/js/modules/misc.ts b/assets/js/modules/misc.ts index 27f9b8d7..4f846ac0 100644 --- a/assets/js/modules/misc.ts +++ b/assets/js/modules/misc.ts @@ -13,17 +13,20 @@ import { getScrollTop, isMobile, isValidDate, scrollIntoView } from '../utils' * - Initialize comment section UI and scroll-into-view. */ export class MiscModule implements MiscService { - private siteTime: ReturnType | undefined + #siteTime: ReturnType | undefined + #core: CoreService - constructor(private readonly core: CoreService) {} + constructor(core: CoreService) { + this.#core = core + } /** Calculate and display the elapsed time since site launch. */ getSiteTime() { const now = new Date() - const run = new Date(this.core.config.siteTime!) + const run = new Date(this.#core.config.siteTime!) const $runTimes = document.querySelector('.run-times') if (!isValidDate(run) || !$runTimes) { - clearInterval(this.siteTime) + clearInterval(this.#siteTime) $runTimes && $runTimes.parentNode!.removeChild($runTimes) return } @@ -38,20 +41,20 @@ export class MiscModule implements MiscService { /** Start the site-time counter with visibility-change pausing. */ initSiteTime() { - if (this.core.config.siteTime) { - this.siteTime = setInterval(() => this.getSiteTime(), 500) + if (this.#core.config.siteTime) { + this.#siteTime = setInterval(() => this.getSiteTime(), 500) document.addEventListener('visibilitychange', () => { if (document.hidden) { - return clearInterval(this.siteTime) + return clearInterval(this.#siteTime) } - this.siteTime = setInterval(() => this.getSiteTime(), 500) + this.#siteTime = setInterval(() => this.getSiteTime(), 500) }, false) } } /** Save and restore scroll position as an automatic bookmark. */ initAutoMark() { - if (!this.core.config.autoBookmark) + if (!this.#core.config.autoBookmark) return window.addEventListener('beforeunload', () => { window.sessionStorage?.setItem(`fixit-bookmark/#${location.pathname}`, String(getScrollTop())) @@ -89,7 +92,7 @@ export class MiscModule implements MiscService { /** Initialize the comment section UI. */ initComment() { - if (!this.core.config.comment?.enable) + if (!this.#core.config.comment?.enable) return if (document.querySelector('#comments')) { @@ -100,7 +103,7 @@ export class MiscModule implements MiscService { }, false) } - if (this.core.config.comment.expired) + if (this.#core.config.comment.expired) document.querySelector('#comments')!.remove() } @@ -109,8 +112,8 @@ export class MiscModule implements MiscService { */ initPostChat() { const initThemeCompatibility = () => { - if (this.core.config.postChat) { - document.body.classList.toggle('dark', this.core.isDark) + if (this.#core.config.postChat) { + document.body.classList.toggle('dark', this.#core.isDark) eventBus.on('fixit:switch-theme', ({ detail }) => { if (!detail.isChanged) return @@ -122,7 +125,7 @@ export class MiscModule implements MiscService { const initPostChatUser = () => { if (!window.postChatUser || !window.postChatConfig || window.postChatConfig.userMode === 'magic') return - window.postChat_theme = this.core.isDark ? 'dark' : 'light' + window.postChat_theme = this.#core.isDark ? 'dark' : 'light' eventBus.on('fixit:switch-theme', ({ detail }) => { if (!detail.isChanged) return diff --git a/assets/js/modules/pwa.ts b/assets/js/modules/pwa.ts index 58bab539..8509aa8a 100644 --- a/assets/js/modules/pwa.ts +++ b/assets/js/modules/pwa.ts @@ -10,11 +10,15 @@ import { eventBus } from '../core/event-bus' * - Show update notification toast and handle refresh. */ export class PWAModule implements PWAService { - constructor(private readonly core: CoreService) {} + #core: CoreService + + constructor(core: CoreService) { + this.#core = core + } /** Register the service worker with update detection and notification binding. */ async setup() { - const pwa = this.core.config.PWA + const pwa = this.#core.config.PWA if (!pwa?.enable || !('serviceWorker' in navigator)) return @@ -49,14 +53,14 @@ export class PWAModule implements PWAService { window.location.reload() }) - this.bindUpdateNotification(registration) + this.#bindUpdateNotification(registration) } /** * Bind the template-rendered update notification toast. * Shows the toast on `fixit:sw-update` and wires the refresh button to skip waiting. */ - private bindUpdateNotification(registration: ServiceWorkerRegistration) { + #bindUpdateNotification(registration: ServiceWorkerRegistration) { const toast = document.querySelector('.sw-update-notification') if (!toast) return diff --git a/assets/js/modules/search/index.ts b/assets/js/modules/search/index.ts index acb30821..7c9560d3 100644 --- a/assets/js/modules/search/index.ts +++ b/assets/js/modules/search/index.ts @@ -19,8 +19,11 @@ export class SearchModule implements SearchService { #openDialog: (() => void) | undefined #closeDialog: (() => void) | undefined #initialized = false + #core: CoreService - constructor(private readonly core: CoreService) {} + constructor(core: CoreService) { + this.#core = core + } /** Create the appropriate search engine based on type. */ #createEngine(type: string, searchConfig: SearchConfig): SearchEngine { @@ -30,9 +33,9 @@ export class SearchModule implements SearchService { case 'fuse': return createFuseEngine(searchConfig) case 'cse': - return createCSEEngine(this.core.config.cse) + return createCSEEngine(this.#core.config.cse) case 'pagefind': - return createPagefindEngine(searchConfig, this.core.config.pagefind!) + return createPagefindEngine(searchConfig, this.#core.config.pagefind!) default: console.warn(`[FixIt] Unknown search type: "${type}". Supported types: algolia, fuse, cse, pagefind.`) return { @@ -45,7 +48,7 @@ export class SearchModule implements SearchService { /** Initialize @algolia/autocomplete-js instance. */ #initAutosearch() { - const searchConfig = this.core.config.search + const searchConfig = this.#core.config.search if (!searchConfig || !this.#engine) return @@ -218,7 +221,7 @@ export class SearchModule implements SearchService { document.querySelector('.search-trigger.desktop')?.addEventListener('click', open) document.querySelector('.search-trigger.mobile')?.addEventListener('click', () => { - this.core.closeMaskOverlay('menu-mobile') + this.#core.closeMaskOverlay('menu-mobile') open() }) @@ -247,7 +250,7 @@ export class SearchModule implements SearchService { /** Initialize the search overlay, autocomplete, and engine-specific logic. */ setup() { - const searchConfig = this.core.config.search + const searchConfig = this.#core.config.search if (!searchConfig || !searchConfig.type) return diff --git a/assets/js/modules/theme.ts b/assets/js/modules/theme.ts index 34fcadd5..bfd86daa 100644 --- a/assets/js/modules/theme.ts +++ b/assets/js/modules/theme.ts @@ -10,9 +10,12 @@ import { eventBus } from '../core/event-bus' * - Persist user preference to localStorage. */ export class ThemeModule implements ThemeService { - private readonly mql = window.matchMedia('(prefers-color-scheme: dark)') + #mql = window.matchMedia('(prefers-color-scheme: dark)') + #core: CoreService - constructor(private readonly core: CoreService) {} + constructor(core: CoreService) { + this.#core = core + } /** * Apply a theme mode and emit the `fixit:switch-theme` event. @@ -20,19 +23,19 @@ export class ThemeModule implements ThemeService { * @param persist - Whether to save the choice to localStorage (default: `true`). */ setThemeMode(mode: string, persist = true) { - const prevIsDark = this.core.isDark - this.core.themeMode = mode + const prevIsDark = this.#core.isDark + this.#core.themeMode = mode document.documentElement.dataset.themeMode = mode - this.core.isDark = mode === 'auto' ? this.mql.matches : mode === 'dark' + this.#core.isDark = mode === 'auto' ? this.#mql.matches : mode === 'dark' if (persist) { window.localStorage?.setItem('theme-mode', mode) } eventBus.emit('fixit:switch-theme', { - isDark: this.core.isDark, + isDark: this.#core.isDark, mode, - isChanged: prevIsDark !== this.core.isDark, + isChanged: prevIsDark !== this.#core.isDark, }) } @@ -49,7 +52,7 @@ export class ThemeModule implements ThemeService { return applyThemeColor(detail.isDark) }) - applyThemeColor(this.core.isDark) + applyThemeColor(this.#core.isDark) } /** Initialize the theme switch button cycle and system preference listener. */ @@ -58,21 +61,21 @@ export class ThemeModule implements ThemeService { document.querySelectorAll('.theme-switch').forEach(($themeSwitch: Element) => { $themeSwitch.addEventListener('click', () => { - const currentIndex = modes.indexOf(this.core.themeMode as typeof modes[number]) + const currentIndex = modes.indexOf(this.#core.themeMode as typeof modes[number]) const nextMode = modes[(currentIndex + 1) % modes.length] this.setThemeMode(nextMode) }, false) }) - this.mql.addEventListener('change', (e: MediaQueryListEvent) => { - if (this.core.themeMode !== 'auto') + this.#mql.addEventListener('change', (e: MediaQueryListEvent) => { + if (this.#core.themeMode !== 'auto') return - const prevIsDark = this.core.isDark - this.core.isDark = e.matches + const prevIsDark = this.#core.isDark + this.#core.isDark = e.matches eventBus.emit('fixit:switch-theme', { - isDark: this.core.isDark, + isDark: this.#core.isDark, mode: 'auto', - isChanged: prevIsDark !== this.core.isDark, + isChanged: prevIsDark !== this.#core.isDark, }) }) } diff --git a/assets/js/modules/toc.ts b/assets/js/modules/toc.ts index dfc8738d..f5a1cee4 100644 --- a/assets/js/modules/toc.ts +++ b/assets/js/modules/toc.ts @@ -16,7 +16,7 @@ const TOC_CONTAINER_IDS = ['toc-content-auto', 'toc-content-static', 'toc-conten * - Clone TOC nodes to detach APlayer event listeners. */ export class TocModule implements TocService { - private activeTocId: string | null = null + #activeTocId: string | null = null /** Get all TOC content containers (auto, static, and drawer). */ #getTocContainers(): HTMLElement[] { @@ -141,8 +141,8 @@ export class TocModule implements TocService { $tocRoots.forEach(($tocRoot) => { this.#applyTocActiveState($tocRoot, activeId) }) - if (this.activeTocId !== activeId) { - this.activeTocId = activeId + if (this.#activeTocId !== activeId) { + this.#activeTocId = activeId if (!isTocStatic()) { const $autoTocRoot = document.querySelector('#toc-content-auto > nav') const $autoTocContainer = document.getElementById('toc-content-auto') @@ -160,7 +160,7 @@ export class TocModule implements TocService { /** Sync TOC layout state: drawer button visibility, height, and active heading. */ #syncTocLayout() { document.querySelector('#toc-drawer-button')?.classList.toggle('hidden', !isTocStatic()) - this.activeTocId = null + this.#activeTocId = null this.syncTocHeight() this.syncTocActiveState() } @@ -229,7 +229,7 @@ export class TocModule implements TocService { this.syncTocHeight() this.syncTocActiveState() const $dialogTocRoot = document.querySelector('#toc-content-drawer > nav')! - this.#scrollActiveTocLinkIntoView($dialogTocRoot, this.activeTocId!, $dialogTocRoot) + this.#scrollActiveTocLinkIntoView($dialogTocRoot, this.#activeTocId!, $dialogTocRoot) ;(document.activeElement as HTMLElement)?.blur() }) dialog.addEventListener('close', () => {