需求
因为自己换工作到了新公司,上周入职,以前没有使用过react框架,虽然前面有学习过react,但是并没有实践经验
这个需求最终的效果是和石墨标题修改实现一样的效果
初始需求
- 文案支持可编辑
- 用户点击位置即光标定位处
- 超过50字读的时候,超出部分进行截断
- 当用户把所有内容删除时,失去焦点时文案设置为 “无文案”三个字
- 编辑区域随着编辑内容的宽度而变化,最大宽度1000px 500px
- 失去焦点时保存文案内容
方案设计
在看到第一眼需求的时候,想到的时候用span和input进行切换,但是这个肯定是满足不了需求中第2点,所以首先这个需求肯定不会是两个 标签切换,只能一个标签承担展示和编辑的功能,第一反应是用html属性contentEditable,就有了我的第一个套方案,后因为需求的第三点实现上存在问题,所以被迫换了方案二(使用input标签),下面我们详细说说为啥弃用方案1选用方案二以及在这过程中遇到的问题。
方案一 span + contentEditable
思路
- 利用h5提供contentEditble,可实现需求点的1/2/5
- 监听focus事件和input时间,可以实现需求点4
- 监听blur事件,可以实现需求点3
但是 需求点中的3点,因为是用字数做的截断,在这个方案中是实现不了的,所以我给出的建议方案是编辑的时候不做截断,非编辑的时候做截断段(是否失去焦点可用作判断是否为编辑态的依据)
代码如下
演示demo:
import React, { useState, useRef, useEffect } from 'react';
import ReactDom from 'react-dom';
interface EditTextProps {
text: string;
// 告知父组件文案已被修改
changeText?: (text: string) => void;
}
const EditText = function (props: EditTextProps) {
useEffect(() => {
setShowText(props.text);
}, [props.text]);
const [showText, setShowText] = useState('');
const [isBlank, setIsBlank] = useState(false);
const [isFocus, setIsFocus] = useState(false);
const textRef = useRef<HTMLDivElement>(null);
const onFocus = () => {
setIsFocus(true)
}
const onInput = () => {
// 避免失去焦点的时候,标题区域明显的闪动
setIsBlank(!textRef.current?.innerHTML);
}
const onBlur = () => {
const newTitle = textRef.current?.innerHTML || '无标题';
const oldTitle = props.text;
setIsFocus(false);
setIsBlank(false);
// 文案更新
if (newTitle !== oldTitle) {
props?.changeText(newTitle);
setShowText(getCharsByLength(newTitle, 50));
}
else {
// 文案不更新
setShowText(getCharsByLength(newTitle, 50));
if(textRef.current) {
textRef.current.innerHTML = getCharsByLength(newTitle, 50)
}
}
}
// 获取前length个字符
const getCharsByLength = (title: string, length: number) => {
const titleLength = title.length;
// 假设都是非中文字符,一个中文字符的宽度可以显示两个非中文字符
let maxLength = length * 2;
const result = [];
for (let i = 0; i < titleLength; i++) {
const char = title[i];
// 中文字符宽度2,非中文字符宽度1
maxLength -= /[\u4e00-\u9fa5]/.test(char) ? 2 : 1;
result.push(char);
if (maxLength <= 0) {
break;
}
}
if (result.length < titleLength) {
result.push('...');
}
return result.join('');
};
return <div className="title">
{isFocus && isBlank ? <span className="title-blank">无标题</span> : ''}
<span
className="title-text"
contentEditable
suppressContentEditableWarning
ref={textRef}
onFocus={onFocus}
onInput={onInput}
onBlur={onBlur}
>{showText}</span>
</div>;
};
在这个方案中遇到的问题
如果在用户修改之前的文案就是【无标题】,此时用户删除了文案所有的内容【将文案置空】,此时失去焦点,根据需求我们应该展示【无标题】,可是在代码逻辑中 进行了setShowText(getCharsByLength(newTitle, 50));
的处理,在不断试探中,发现修改前后的showText
一摸一样,无法触发dom的更新,针对这个问题我找到了两个解决方式
- 方式一 在不需要更新标题,用户触发了失去焦点,但是并没有修改标题时,先把showText设置为空,在setTimeout中设置会以前的标题。
尝试了一下这个方案,从使用角度来说并不会特别明显的闪动。不过个人觉得这个方案代码看着很怪异
const onBlur = () => {
const newTitle = textRef.current?.innerHTML || '无标题';
const oldTitle = props.text;
setIsFocus(false);
setIsBlank(false);
// 文案更新
if (newTitle !== oldTitle) {
props?.changeText(newTitle);
setShowText(getCharsByLength(newTitle, 50));
}
else {
// 文案不更新
setShowText('');
setTimeout(() => {
setShowText(getCharsByLength(newTitle, 50));
}, 0)
}
}
- 方式二 利用ref
const onBlur = () => {
const newTitle = textRef.current?.innerHTML || '无标题';
const oldTitle = props.text;
setIsFocus(false);
setIsBlank(false);
// 文案更新
if (newTitle !== oldTitle) {
props?.changeText(newTitle);
setShowText(getCharsByLength(newTitle, 50));
}
else {
// 文案不更新
setShowText(getCharsByLength(newTitle, 50));
if(textRef.current) {
textRef.current.innerHTML = getCharsByLength(newTitle, 50)
}
}
}
存在的问题
- 无法用字数做限制
- 如果用宽度做限制,可以出现截断的效果,但是内容无法滑动
方案二 直接用input处理展示和编辑
采用修改input框样式的方法,让input展示和可编辑文案。整体的效果和文章开头展示的效果一致。 canEdit
这个参数时我后面加的,用来控制EditText
组件是否可以编辑。遇到的问题见面后面。 演示demo:
import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';
interface EditTextProps {
text: string;
canEdit?: boolean;
changeText?: (text: string) => void;
}
function EditText(props: EditTextProps) {
// 根据span获取宽度
const witdthRef = useRef<HTMLDivElement>(null);
const [showText, setShowText] = useState('');
const [isFocus, setIsFocus] = useState(false);
const [inputWith, setInputWith] = useState(100);
const minTitleWidth = 70;
const maxTitleWidth = 500;
useEffect(() => {
setShowText(props.text);
}, [props.text]);
useLayoutEffect(() => {
dealInputWidth();
}, [showText]);
const dealInputWidth = () => {
const offsetWidth = witdthRef?.current?.offsetWidth || minTitleWidth;
// +5 防止出现 截断
const width = offsetWidth < maxTitleWidth ? offsetWidth + 5 : maxTitleWidth;
setInputWith(width);
};
const titleFocus = () => {
setIsFocus(true);
};
const titleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTitle = e.target.value;
setShowText(newTitle);
};
const titleBlur = () => {
const newTitle = showText || '无标题';
const oldTitle = props.text;
setIsFocus(false);
if (showText !== oldTitle) {
setShowText(newTitle);
setIsFocus(false);
if (props?.changeText) {
props.changeText(newTitle);
}
} else {
setIsFocus(false);
setShowText(newTitle);
}
};
return (
<div className='wrap'>
{props.canEdit ? (
<input
value={showText}
style={{ width: inputWith }}
onFocus={titleFocus}
onChange={titleInput}
onBlur={titleBlur}
className='input'
placeholder="无标题"
/>
) : (
''
)}
{}
<span ref={witdthRef} className={props.canEdit ? 'width' : 'text'}>
{showText}
</span>
</div>
);
}
踩到的坑
input自带宽度,无法实现宽度随着文案的改变而改变。
在方案一做出来后,就和UI进行了沟通在【编辑的时候用字数做截断实现不了】,给出了一个建议的方案【编辑的时候不做截断】,但是设计同学觉得不截断的方案过丑,,,,,然后她就说能实现 【石墨标题编辑】时,类似的效果交互吗???于是我就开启了研究石墨的效果的征途中。
只发现 石墨用了一个input实现了不错的效果,input后面放了一个span标签,我体验的时候,一直在想为什么会有一个span标签呢??(小朋友,是不是满脸疑问)
直到我发现input自带宽度,无法随着内容的宽度的改变而改变。此时才恍然大悟span标签的作用。
我也采用了利用span标签的宽度的方式来控input输入内容的宽度。
开玩笑,咋可能这么顺利,我遇到了第二个问题
用useEffect 来监控 witdthRef.current.offsetWidth
时,拿到的是上次文案的宽度 经过查阅资料,我发现了useLayoutEffect
这个hook,真香
以上就是react编写可编辑标题示例详解的详细内容,更多关于react编写可编辑标题的资料请关注编程网其它相关文章!