广告位联系
返回顶部
分享到

react编写可编辑标题示例介绍

JavaScript 来源:互联网 作者:佚名 发布时间:2022-08-23 19:51:30 人浏览
摘要

需求 因为自己换工作到了新公司,上周入职,以前没有使用过react框架,虽然前面有学习过react,但是并没有实践经验 这个需求最终的效果是和石墨标题修改实现一样的效果 初始需求

需求

因为自己换工作到了新公司,上周入职,以前没有使用过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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

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中设置会以前的标题。

尝试了一下这个方案,从使用角度来说并不会特别明显的闪动。不过个人觉得这个方案代码看着很怪异

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

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,真香


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://juejin.cn/post/7133896956409348132
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计