
本文介绍如何在网页排版工具中,将可拖拽元素(.resizable)的宽度实时转换为 CSS 自定义属性 --_typecsset-text-measure,并与 双向绑定,确保拖拽改变行宽时输入框值自动更新,反之亦然。
本文介绍如何在网页排版工具中,将可拖拽元素(`.resizable`)的宽度实时转换为 css 自定义属性 `--_typecsset-text-measure`,并与 `` 双向绑定,确保拖拽改变行宽时输入框值自动更新,反之亦然。
在响应式 typography 应用中,用户常需通过两种互补方式控制文本行宽(measure):一是直接输入数值(如 80rem),二是直观拖拽可视标尺。要实现真正一致的用户体验,二者必须双向同步——即拖拽更新 CSS 变量并回填输入框,而手动输入也应即时反映到 DOM 尺寸上。
✅ 核心实现逻辑
我们依赖两个关键技术:
-
ResizeObserver:监听.resizable元素尺寸变化(因resize: inline触发); -
CSS 自定义属性 +
document.documentElement.style.setProperty():统一驱动样式与状态。
1. 拖拽 → 输入框 + CSS 变量(单向同步)
const observeMeasure = new ResizeObserver(mutations => {
const widthPx = mutations[0].contentRect.width;
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
const widthRem = (widthPx / rootFontSize).toFixed(1); // 保留一位小数更符合 UX
// 更新 CSS 变量(单位:rem)
document.documentElement.style.setProperty('--_typecsset-text-measure', `${widthRem}rem`);
// 同步输入框值(ID 为 'measure')
const input = document.getElementById('measure');
if (input) input.value = widthRem;
});
observeMeasure.observe(document.querySelector('.resizable'));
⚠️ 注意:
contentRect.width返回的是像素值,需除以根字体大小(1rem对应的 px 值)才能转为rem,这是跨设备/缩放兼容的关键。
2. 输入框 → CSS 变量(补充双向性)
你已有的 numberInputs 监听逻辑基本正确,但需微调以支持 measure 输入:
const numberInputs = document.querySelectorAll('input[type="number"]');
numberInputs.forEach(input => {
input.addEventListener('input', function() {
const uom = this.dataset.uom || '';
const value = this.value.trim() === '' ? '0' : this.value;
// 关键:对 measure 输入,确保单位为 rem(即使 data-uom 已设,也显式保障)
const finalValue = this.id === 'measure'
? `${value}rem`
: `${value}${uom}`;
document.documentElement.style.setProperty(`--_${this.name}`, finalValue);
});
});
3. 输入框 → 拖拽元素尺寸(可选但推荐)
若希望手动输入后,.resizable 宽度也立即匹配(视觉反馈更强),可主动设置其 inline-size:
// 在上述 input 事件处理器末尾添加:
if (this.id === 'measure') {
const dragEl = document.querySelector('.resizable');
const targetWidthPx = parseFloat(value) * rootFontSize;
dragEl.style.inlineSize = `${targetWidthPx}px`;
}
? 提示:为避免
ResizeObserver重复触发导致循环,建议在设置dragEl.style.inlineSize前临时unobserve(),设置后再observe()—— 但对 typography 工具而言,轻微延迟通常可接受,简洁性优先。
? 完整整合代码片段
<!-- 确保 .resizable 内 input 具有唯一 ID 和 class -->
<section class="resizable"><label class="rhythm-xs">
<h2>Text Measure</h2>
<input id="measure" class="icon-ruler steppers-none" type="number" value="80" step="0.5" min="10" max="150" data-uom="rem" name="typecsset-text-measure"><div class="cluster">
<h4>rem</h4>
<h4>drag ↘ </h4>
</div>
</label>
</section>
// 统一初始化
document.addEventListener('DOMContentLoaded', () => {
// 1. 数字输入同步
document.querySelectorAll('input[type="number"]').forEach(input => {
input.addEventListener('input', function() {
const uom = this.dataset.uom || '';
const val = this.value || '0';
const cssVal = this.id === 'measure' ? `${val}rem` : `${val}${uom}`;
document.documentElement.style.setProperty(`--_${this.name}`, cssVal);
// 可选:反向驱动拖拽尺寸
if (this.id === 'measure') {
const drag = document.querySelector('.resizable');
const rootFS = parseFloat(getComputedStyle(document.documentElement).fontSize);
drag.style.inlineSize = `${parseFloat(val) * rootFS}px`;
}
});
});
// 2. 拖拽同步
const resizeObs = new ResizeObserver(mutations => {
const w = mutations[0].contentRect.width;
const r = parseFloat(getComputedStyle(document.documentElement).fontSize);
const rem = (w / r).toFixed(1);
document.documentElement.style.setProperty('--_typecsset-text-measure', `${rem}rem`);
document.getElementById('measure').value = rem;
});
resizeObs.observe(document.querySelector('.resizable'));
});
✅ 注意事项与最佳实践
-
单位一致性:始终以
rem作为--_typecsset-text-measure的单位,CSS 中max-inline-size: var(--_typecsset-text-measure)才能正确响应。 -
精度控制:
toFixed(1)避免浮点误差导致输入框显示80.00000000000001,提升可读性。 -
性能考量:
ResizeObserver是异步且高效的选择,远优于resize事件或轮询;无需防抖。 -
无障碍增强:为
.resizable添加aria-label="Adjust text measure",并确保键盘可聚焦(tabindex="0")及keydown支持方向键微调(进阶需求)。 -
CSS 重置:确保
.resizable元素无min-width/max-width干扰拖拽行为,resize: inline需配合overflow: auto|scroll。
通过以上方案,你的 typography 工具即可实现专业级的双向联动——设计师既可精确输入数值,也能直觉拖拽调整,所有变更实时、一致、可预测。










