当前位置 : 主页 > 网络编程 > JavaScript >

react编写可编辑标题示例详解

来源:互联网 收集:自由互联 发布时间:2023-02-08
目录 需求 初始需求 方案设计 方案一 span + contentEditable 思路 代码如下 在这个方案中遇到的问题 存在的问题 方案二 直接用input处理展示和编辑 踩到的坑 需求 因为自己换工作到了新公
目录
  • 需求
    • 初始需求
  • 方案设计
    • 方案一 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编写可编辑标题的资料请关注易盾网络其它相关文章!

      网友评论