需求背景
最近写项目碰到一个描述简单,但实现起来比较复杂的需求,且产品还没给具体实现方案。即:前端实现选择‘因子’(指标)配置公式,公式要求:四则运算+括号。借鉴网上各位前辈经验制定了如下实现方式:1.输入'$'弹出弹框选择因子.
2.选择因子回填,根据需求输入+, -, *, /, (, ).
技术栈
umi4.0 + antd4.0 + js原生(90%)写在前面
本人是一枚刚入行不久的前端新手,这个小功能断断续续花了三天左右才实现,写这篇文章的目的是回顾梳理下实现这个输入框的实现过程,并且希望能给实现类似功能焦头烂额的你提供一点思路,如有问题,欢迎指正。所需知识
- 给div标签设置contenteditable={true}会使其变为可编辑html
- onCompositionStart 在输入中文开始时触发, onCompositionEnd 在输入中文结束时触发; ⚠️:这是非常重要的一点,只有onCompositionEnd才能拿到在中文输入法下的值,input则拿不到
- 一些 正则 知识;
- Window.getSelection();
- dom.dispatchEvent(new Event('input', { bubbles: true }));手动触发input
正文
1.给div标签加上contenteditable
<div
className='frontLineItem'
contentEditable
ref={divRef}
onClick={getRecordCoordinates}
onKeyDown={keydownEv}
onCompositionEnd={handleChineseInput}
onInput={handleInputValue}
onPaste={(e) => { e.preventDefault() }}
/>
.frontLineItem {
position: relative;
border: 1px solid #d9d9d9;
padding: 5px;
height: 80px;
outline: none;
border-radius: 2px;
// 利用伪元素伪造placeholder
&:empty::after {
content: '允许输入数字, $, +-*/ ()';
color: #d9d9d9;
position: absolute;
top: 4px;
left: 8px;
}
}
- 获取光标位置
const getRecordCoordinates = () => {
try {
const selection = window.getSelection() as Selection;
const range = selection?.getRangeAt(0);
lightPosition.current = {
selection,
range, // 返回range对象
};
console.log('totalRange',lightPosition.current.range,selection)
} catch (error) {
throw new Error('获取光标位置失败');
}
};
- 监听键盘输入事件
const keydownEv = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === '$') {
e.preventDefault();
getRecordCoordinates();
Modal.confirm(modalConfig);
} else {
const allowReg = /^[0-9.+\-*/()]|(Backspace)|(ArrowLeft)|(ArrowRight)|(ArrowDown)|(ArrowUp)|(Shift)+$/;
if (!allowReg.test(e.key)) {
e.preventDefault();
}
}
};
4.监听中文输入,在中文输入完成时将输入的中文截掉
const handleChineseInput = (e: CompositionEvent<HTMLDivElement>) => {
const curChinese = e.data;
// 会造成光标到前面
divRef.current!.innerHTML = divRef.current!.innerHTML.replace(curChinese, '');
// 处理边缘情况:input为空时输入中文
divRef.current!.dispatchEvent(new Event('input', { bubbles: true }));
// 光标到最后(TODO: 光标定位)
const { range, selection } = lightPosition.current!;
range?.selectNodeContents(divRef.current!);
range?.collapse(false);
selection?.removeAllRanges();
selection?.addRange(range!);
// 更新光标
getRecordCoordinates();
}
5.弹框的配置(此处用的是antd组件,可根据需求自行更改)
const modalConfig: ModalFuncProps = {
icon: null,
title: <div style={{ marginBottom: 10 }}> 因子选择</div>,
content: <Form form={factorForm} colon={false}>
<Form.Item label=' ' name='factorGuid'>
<Select placeholder='选择因子'>
{
fackedFactorObjs.map((f) => (
<Option key={f.id} value={f.id}>
{f.name}
</Option>
))
}
</Select>
</Form.Item>
</Form>,
onOk() {
const { selection, range } = lightPosition.current!;
const inputNode = document.createElement('input');
inputNode.value = `${fackedFactorObjs.find((f) => f.id === factorForm.getFieldValue('factorGuid'))!.name}`; // $的文本信息
inputNode.type = 'button';
inputNode.dataset.id = `${factorForm.getFieldValue('factorGuid')}`; // 用户ID、为后续解析富文本提供
inputNode.className = 'tag';
const frag = document.createDocumentFragment();
frag.appendChild(inputNode);
if (range) {
range.insertNode(frag);
}
// 解决插入结点不触发input事件
divRef.current!.dispatchEvent(new Event('input', { bubbles: true }));
selection.removeAllRanges(); // 移除当前光标
selection.addRange(range); // 还原光标位置
selection.collapseToEnd(); // addRange之后光标处于选中状态,需要将光标移动至最末端
getRecordCoordinates(); // 更新存储的光标信息
},
};
- 由于该组件要配合antd的Form使用,antd官网的描述如下
onChange可以实现,即form.getFeildsvalue()不受影响。由于div没有value属性,需用dangerouslySetInnerHTML代替,但如此就会引发新的问题,每次输入都会导致光标前移,故而给出父组件传入ref拿到整个dom的方法来弥补不能form.setFeildsValue()的缺陷
写在后面
实现该组件的目的是为了实现一个表单控件传值给后端,不同的后端需要不同的数据结构,可参照我的处理方式这里,这里也就不细说了。点击这里查看源码