mirror of
https://github.com/rememberber/MooTool.git
synced 2026-09-01 15:34:30 +08:00
[next]feat: Shuishouji and JSON Workbench need to support font switching functionality.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { useMemo } from 'react'
|
||||
import { cssFontFamily, fontSelectOptions, useSystemFontFamilies } from '@/shared/fonts/systemFonts'
|
||||
|
||||
type FontSelectProps = {
|
||||
value: string
|
||||
ariaLabel: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
emptyLabel?: string
|
||||
labels?: Record<string, string>
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function FontSelect({
|
||||
value,
|
||||
ariaLabel,
|
||||
disabled = false,
|
||||
className = '',
|
||||
emptyLabel,
|
||||
labels = {},
|
||||
onChange
|
||||
}: FontSelectProps) {
|
||||
const fonts = useSystemFontFamilies()
|
||||
const options = useMemo(() => fontSelectOptions(fonts, value), [fonts, value])
|
||||
|
||||
return (
|
||||
<select
|
||||
className={className}
|
||||
aria-label={ariaLabel}
|
||||
title={ariaLabel}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
>
|
||||
{emptyLabel !== undefined ? <option value="">{emptyLabel}</option> : null}
|
||||
{options.map((font) => (
|
||||
<option key={font} value={font} style={{ fontFamily: cssFontFamily(font) }}>
|
||||
{labels[font] ?? font}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { cssFontFamily, fallbackEditorFonts, fontSelectOptions, listSystemFontFamilies } from './systemFonts'
|
||||
|
||||
describe('cssFontFamily', () => {
|
||||
it('uses the app UI stack for empty and system-ui values', () => {
|
||||
expect(cssFontFamily('')).toBe('var(--app-font-family), system-ui, sans-serif')
|
||||
expect(cssFontFamily('system-ui')).toBe('var(--app-font-family), system-ui, sans-serif')
|
||||
})
|
||||
|
||||
it('keeps a monospace fallback stack for ui-monospace', () => {
|
||||
expect(cssFontFamily('ui-monospace')).toBe('ui-monospace, SFMono-Regular, Menlo, Consolas, monospace')
|
||||
})
|
||||
|
||||
it('quotes font names that need CSS escaping', () => {
|
||||
expect(cssFontFamily('PingFang SC')).toBe('"PingFang SC", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace')
|
||||
expect(cssFontFamily('Font "Demo"')).toBe('"Font \\"Demo\\"", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fontSelectOptions', () => {
|
||||
it('keeps ui-monospace first and includes the current font', () => {
|
||||
expect(fontSelectOptions(['Georgia', 'ui-monospace', 'Arial'], 'Comic Sans MS')).toEqual([
|
||||
'ui-monospace',
|
||||
'Arial',
|
||||
'Comic Sans MS',
|
||||
'Georgia'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('listSystemFontFamilies', () => {
|
||||
it('merges queryLocalFonts families with the fallback list', async () => {
|
||||
vi.stubGlobal('queryLocalFonts', async () => [{ family: ' Custom Editor Font ' }, { family: '' }])
|
||||
try {
|
||||
const fonts = await listSystemFontFamilies()
|
||||
expect(fonts).toContain('Custom Editor Font')
|
||||
expect(fonts).toEqual(expect.arrayContaining(fallbackEditorFonts))
|
||||
} finally {
|
||||
vi.unstubAllGlobals()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const fallbackEditorFonts = [
|
||||
'ui-monospace',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'Courier New',
|
||||
'JetBrains Mono',
|
||||
'PingFang SC',
|
||||
'Hiragino Sans GB',
|
||||
'Microsoft YaHei',
|
||||
'等线',
|
||||
'Songti SC',
|
||||
'SimSun',
|
||||
'Georgia',
|
||||
'Times New Roman',
|
||||
'SF Pro Text',
|
||||
'Helvetica Neue',
|
||||
'Arial',
|
||||
'system-ui'
|
||||
]
|
||||
|
||||
type LocalFontData = {
|
||||
family: string
|
||||
}
|
||||
|
||||
export async function listSystemFontFamilies(): Promise<string[]> {
|
||||
const families = new Set(fallbackEditorFonts)
|
||||
try {
|
||||
for (const font of await queryInstalledFonts()) {
|
||||
const family = font.family.trim()
|
||||
if (family) families.add(family)
|
||||
}
|
||||
} catch {
|
||||
// Local Font Access may be unavailable in tests or locked-down environments.
|
||||
}
|
||||
return sortFontFamilies([...families])
|
||||
}
|
||||
|
||||
export function cssFontFamily(fontName: string): string {
|
||||
const value = fontName.trim()
|
||||
if (!value || value === 'system-ui') return 'var(--app-font-family), system-ui, sans-serif'
|
||||
if (value === 'ui-monospace') return 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
||||
const quoted = /[\s,"'\\]/.test(value) ? `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"` : value
|
||||
return `${quoted}, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
|
||||
}
|
||||
|
||||
export function fontSelectOptions(fonts: string[], current = ''): string[] {
|
||||
const unique = new Set(fonts.filter(Boolean))
|
||||
if (current.trim()) unique.add(current.trim())
|
||||
const rest = sortFontFamilies([...unique].filter((font) => font !== 'ui-monospace'))
|
||||
return unique.has('ui-monospace') ? ['ui-monospace', ...rest] : rest
|
||||
}
|
||||
|
||||
export function useSystemFontFamilies(): string[] {
|
||||
const [fonts, setFonts] = useState(fallbackEditorFonts)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void listSystemFontFamilies().then((next) => {
|
||||
if (!cancelled) setFonts(next)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return fonts
|
||||
}
|
||||
|
||||
function sortFontFamilies(fonts: string[]): string[] {
|
||||
return [...fonts].sort((left, right) => left.localeCompare(right, undefined, { sensitivity: 'base' }))
|
||||
}
|
||||
|
||||
async function queryInstalledFonts(): Promise<LocalFontData[]> {
|
||||
const query = (globalThis as { queryLocalFonts?: () => Promise<LocalFontData[]> }).queryLocalFonts
|
||||
if (typeof query !== 'function') return []
|
||||
return query()
|
||||
}
|
||||
Reference in New Issue
Block a user