refactor(assets): use ES6 # private fields instead of TS private

Convert all TypeScript `private` and `private readonly` declarations to
ES6 `#` syntax for true runtime privacy. TypeScript's `private` keyword
only provides compile-time checking but fields remain accessible in
compiled JavaScript.
This commit is contained in:
Cell
2026-07-23 14:33:19 +08:00
parent f5509e5b82
commit a5d24f395e
11 changed files with 115 additions and 89 deletions
+4 -4
View File
@@ -22,14 +22,14 @@ export type Handler<T> = T extends void
/** Typed event bus — wraps DOM CustomEvents with type-safe emit/on/off. */
export class TypedEventBus {
private target = document
#target = document
on<K extends keyof FixItEventMap>(event: K, handler: Handler<FixItEventMap[K]>): void {
this.target.addEventListener(event as string, handler as EventListener)
this.#target.addEventListener(event as string, handler as EventListener)
}
off<K extends keyof FixItEventMap>(event: K, handler: Handler<FixItEventMap[K]>): void {
this.target.removeEventListener(event as string, handler as EventListener)
this.#target.removeEventListener(event as string, handler as EventListener)
}
emit<K extends keyof FixItEventMap>(
@@ -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),
+13 -10
View File
@@ -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<HTMLElement, HTMLElement>()
$footnoteRefs.forEach(($ref) => {
if (this.core.config.tooltip) {
if (this.#core.config.tooltip) {
const $link = $ref.querySelector<HTMLAnchorElement>('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)
}
+16 -16
View File
@@ -16,8 +16,8 @@ export class CoreModule implements CoreService {
themeMode: string
isDark: boolean
private activeMaskOverlay: string | null = null
private readonly maskOverlays = new Map<string, MaskOverlayHandler>()
#activeMaskOverlay: string | null = null
#maskOverlays = new Map<string, MaskOverlayHandler>()
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)
}
}
+7 -3
View File
@@ -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)
}
}
+11 -9
View File
@@ -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)
}
+7 -3
View File
@@ -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<HTMLElement>('.menu-item>.nested-item').forEach(($nestedItem) => {
+17 -14
View File
@@ -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<typeof setInterval> | undefined
#siteTime: ReturnType<typeof setInterval> | 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<HTMLElement>('.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
+8 -4
View File
@@ -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<HTMLElement>('.sw-update-notification')
if (!toast)
return
+9 -6
View File
@@ -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
+18 -15
View File
@@ -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,
})
})
}
+5 -5
View File
@@ -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<HTMLElement>('#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<HTMLElement>('#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<HTMLElement>('#toc-content-drawer > nav')!
this.#scrollActiveTocLinkIntoView($dialogTocRoot, this.activeTocId!, $dialogTocRoot)
this.#scrollActiveTocLinkIntoView($dialogTocRoot, this.#activeTocId!, $dialogTocRoot)
;(document.activeElement as HTMLElement)?.blur()
})
dialog.addEventListener('close', () => {