JavaScript
主页 > 网络编程 > JavaScript >

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

2022-08-23 | 佚名 | 点击:

需求

因为自己换工作到了新公司,上周入职,以前没有使用过react框架,虽然前面有学习过react,但是并没有实践经验

这个需求最终的效果是和石墨标题修改实现一样的效果

初始需求

方案设计

在看到第一眼需求的时候,想到的时候用span和input进行切换,但是这个肯定是满足不了需求中第2点,所以首先这个需求肯定不会是两个 标签切换,只能一个标签承担展示和编辑的功能,第一反应是用html属性contentEditable,就有了我的第一个套方案,后因为需求的第三点实现上存在问题,所以被迫换了方案二(使用input标签),下面我们详细说说为啥弃用方案1选用方案二以及在这过程中遇到的问题。

方案一 span + contentEditable

思路

但是 需求点中的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的更新,针对这个问题我找到了两个解决方式

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

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)

    }

  }

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

原文链接:https://juejin.cn/post/7133896956409348132
相关文章
最新更新