目录
- 需求
- 初始需求
- 方案设计
- 方案一 span + contentEditable
- 思路
- 代码如下
- 在这个方案中遇到的问题
- 存在的问题
- 方案二 直接用input处理展示和编辑
- 踩到的坑
需求
因为自己换工作到了新公司,上周入职,以前没有使用过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编写可编辑标题的资料请关注易盾网络其它相关文章!