|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
import { useEffect, useRef, type ReactNode } from 'react' /** * 与 index.html / postcss-pxtorem 一致: * - 设计稿 375 * - html fontSize = (width / 375) * 100 * - 设计稿 px → rem:px / 100 */ const DESIGN_ROOT_VALUE = 100 const MIN_DESIGN_FONT_SIZE = 8 const FIT_FRAME_BUDGET_MS = 6 type FitTask = () => void const activeFitTasks = new Set<FitTask>() const pendingFitTasks = new Set<FitTask>() let fitFrameId = 0 let resizeListening = false let fontsReadyListening = false const flushFitTasks = () => { fitFrameId = 0 const startedAt = performance.now() for (const task of pendingFitTasks) { pendingFitTasks.delete(task) task() if (performance.now() - startedAt >= FIT_FRAME_BUDGET_MS) break } if (pendingFitTasks.size > 0) { fitFrameId = window.requestAnimationFrame(flushFitTasks) } } const scheduleFitTask = (task: FitTask) => { pendingFitTasks.add(task) if (!fitFrameId) { fitFrameId = window.requestAnimationFrame(flushFitTasks) } } const scheduleAllFitTasks = () => { activeFitTasks.forEach(scheduleFitTask) } const subscribeFitTask = (task: FitTask) => { activeFitTasks.add(task) scheduleFitTask(task) if (!resizeListening) { resizeListening = true window.addEventListener('resize', scheduleAllFitTasks, false) } if (!fontsReadyListening && document.fonts?.ready) { fontsReadyListening = true void document.fonts.ready.then(scheduleAllFitTasks) } return () => { activeFitTasks.delete(task) pendingFitTasks.delete(task) if (activeFitTasks.size === 0 && resizeListening) { resizeListening = false window.removeEventListener('resize', scheduleAllFitTasks, false) } } } interface AutoFitTextProps { children: ReactNode className?: string } const designPxToRem = (designPx: number) => `${designPx / DESIGN_ROOT_VALUE}rem` /** * 取所有可观测宽度的最小值: * - 元素真实布局宽度 * - 父容器扣除 padding 后的内容宽度 * - flex/grid 宿主扣除兄弟后的分配宽度 * * 不能只用宿主分配宽度:父容器有 padding 时会高估,导致测量判定已 * 放下、实际却被 overflow 裁掉最后一个字符。 */ const resolveConstraintWidth = (el: HTMLElement): number => { const parent = el.parentElement if (!parent) return 0 const widths: number[] = [] const ownWidth = el.getBoundingClientRect().width if (ownWidth > 0) widths.push(ownWidth) const parentStyle = window.getComputedStyle(parent) const parentContentWidth = parent.clientWidth - (parseFloat(parentStyle.paddingLeft) || 0) - (parseFloat(parentStyle.paddingRight) || 0) if (parentContentWidth > 0) widths.push(parentContentWidth) const host = parent.parentElement if (host && host.clientWidth > 0) { let siblingsWidth = 0 const children = host.children for (let i = 0; i < children.length; i += 1) { const child = children[i] as HTMLElement if (child === parent) continue siblingsWidth += child.offsetWidth } const margin = (parseFloat(parentStyle.marginLeft) || 0) + (parseFloat(parentStyle.marginRight) || 0) const next = host.clientWidth - siblingsWidth - margin if (next > 0) widths.push(next) } if (widths.length === 0) return 0 return Math.max(0, Math.min(...widths)) } /** * 默认继承父级字号;超出后按设计稿字号逐步 -1 缩小, * 最小 8px;仍超出再省略号。 */ export const AutoFitText = ({ children, className }: AutoFitTextProps) => { const ref = useRef<HTMLSpanElement>(null) useEffect(() => { const el = ref.current if (!el) return let cancelled = false const fit = () => { if (cancelled) return el.style.fontSize = '' el.style.maxWidth = '' el.classList.remove('is-ellipsis') const constraint = resolveConstraintWidth(el) if (constraint <= 0) return el.style.maxWidth = `${constraint}px` if (el.clientWidth <= 0) return if (el.scrollWidth <= el.clientWidth) { el.style.maxWidth = '' return } const rootPx = parseFloat( window.getComputedStyle(document.documentElement).fontSize, ) || DESIGN_ROOT_VALUE const inheritedCssPx = parseFloat( window.getComputedStyle(el).fontSize, ) if (!(inheritedCssPx > 0)) { el.classList.add('is-ellipsis') return } let designSize = Math.round((inheritedCssPx / rootPx) * DESIGN_ROOT_VALUE) while ( designSize > MIN_DESIGN_FONT_SIZE && el.scrollWidth > el.clientWidth ) { designSize -= 1 el.style.fontSize = designPxToRem(designSize) } if (el.scrollWidth > el.clientWidth) { el.classList.add('is-ellipsis') } else { // 缩到能放下后去掉 maxWidth,避免多余裁切 el.style.maxWidth = '' } } const unsubscribe = subscribeFitTask(fit) return () => { cancelled = true unsubscribe() } }, [children]) return ( <span ref={ref} className={`trade-order-bottom__auto-fit-text${ className ? ` ${className}` : '' }`} > {children} </span> ) } |
