有趣的 contentEditable

简介: 以前在知乎看到一篇关于《一行代理可以做什么?》的回答:当时试了一下确实很好玩,于是每次都可以在妹子面前秀一波操作,在他们惊叹的目光中,我心里开心地笑了——嗯,又让一个不懂技术的人发现到了程序的美🐶,咳咳。一直以来,我都觉得这个属性只是为了存在而存在的,然而在今天接到的需求之后,我发现这个感觉没什么用的属性竟然完美地解决了我的需求。

以前在知乎看到一篇关于《一行代理可以做什么?》的回答:


image.png


当时试了一下确实很好玩,于是每次都可以在妹子面前秀一波操作,在他们惊叹的目光中,我心里开心地笑了——嗯,又让一个不懂技术的人发现到了程序的美🐶,咳咳。

一直以来,我都觉得这个属性只是为了存在而存在的,然而在今天接到的需求之后,我发现这个感觉没什么用的属性竟然完美地解决了我的需求。


需求




需求很简单,在输入框里添加按钮就好了。这种功能一般用于邮件群发,这里的按钮“姓名”其实就是一个变量,后端应该要自动填充真实用户的姓名,然后再把邮件发给用户的。


image.png


问题




这个需求乍一看感觉可以用 position: relative + position: absolute 来完成。但是细想就不太可能:按钮肯定会覆盖输入内容的,而且单单一个删除“姓名”按钮这个功能就很难做。


再说只用 <textarea> 也不可能实现,因为<textarea>里就不可能存在输入 button 的情况。


我想另一个可能就是以<div>为底,在<div>最后加一个宽度为1px的<input>,然后用双向绑定去实现添加按钮和修改文本功能,用这个1px宽度的<input>来实现 focus 和blur功能。但是感觉也特别难实现。


最后在一篇 stackoverflow 里找到了答案:Button inside TextArea in HTML

然后我又搜了一下看到了这个库:react-contenteditable


解决方案




看到 contentEditable 的时候还是有点震惊的,毕竟这个一直被我用来秀来秀去的属性竟然在这一天解决了我的问题。


这个库用起来也很有意思,使用函数组件的时候,它不像我们普通那里一个 value 一个 onChange 就搞定了,而它需要我们传一个 innerRef 来控制里面的文本。


function App() {
  const innerRef = useRef<HTMLElement>(null);
  const value = useRef<string>('');
  const onChange = (event: ContentEditableEvent) => {
    value.current = event.target.value;
  }
  const onAddButton = () => {
    if (!innerRef.current) {
      return;
    }
    innerRef.current.innerHTML += '&nbsp;<button contenteditable="false">姓名</button>&nbsp;'
  }
  return (
    <div className="App">
      <ContentEditable 
        style={{ border: '1px solid black', height: 100 }} 
        innerRef={innerRef} 
        html={value.current} 
        onChange={onChange} 
      />
      <button onClick={onAddButton}>添加姓名</button>
    </div>
  );
}


细看 react-contentEditable 源码




上面说到的使用 ref 来控制文本的变化让我好奇里面到底是怎么实现的,所以我把他的 github clone 了下来,发现这里面的实现确实不太简单。Github 在这里。


render 函数


因为我们使用 contentEditable 来实现输入输出,所以几乎任何元素都是可以的,因此,这个组件允许我们传入 tagName 来指定要以哪个元素为基底。


render() {
  const { tagName, html, innerRef, ...props } = this.props;
  return React.createElement(
    tagName || 'div',
    {
      ...props,
      ref: typeof innerRef === 'function' ? (current: HTMLElement) => {
        innerRef(current)
        this.el.current = current
      } : innerRef || this.el,
      onInput: this.emitChange,
      onBlur: this.props.onBlur || this.emitChange,
      onKeyUp: this.props.onKeyUp || this.emitChange,
      onKeyDown: this.props.onKeyDown || this.emitChange,
      contentEditable: !this.props.disabled,
      dangerouslySetInnerHTML: { __html: html }
    },
    this.props.children);
}


这里的 render 函数就是为了一个指定渲染哪个函数,同时绑定一些事件,是否开启 contentEditable 属性,并传入 props。



我们还观察到这里的值其实是通过 dangerouslySetInnerHTML: { __html: html } 来展示的。


那既然都是 dangerously 了,那我们当然就要想到去防止脚本注入了嘛,所以源码也对值进行 normalize 了:


function normalizeHtml(str: string): string {
  return str && str.replace(/&nbsp;|\u202F|\u00A0/g, ' ');
}


事件


还有一个值得注意的点是其实除了 <input><textarea> 之外,<div> 也是可以触发 onInput 事件的。

比如我自己也尝试实现了一下:


const VarInput: FC<IProps> = (props) => {
  const { value, tag, disabled, onInput, ref, ...restProps } = props;
  const innerRef = useRef(null);
  const curtRef = ref || innerRef;
  const emitChange = () => {
    const callbackValue: string = curtRef.current ? curtRef.current.innerHTML : '';
    onInput!(callbackValue);
  }
  const varInputProps = {
    ...restProps,
    ref: curtRef,
    contentEditable: !disabled,
    onInput: emitChange,
    dangerouslySetInnerHTML: { __html: value }
  }
  return createElement(tag || 'div', varInputProps);
}


使用的时候


function App() {
  const [value] = useState('');
  const onChange = (value: string) => {
    console.log(value); // 打印 value
  }
  return (
    <div className="App">
      <VarInput value={value} onInput={onChange} />
    </div>
  );


然而,当我只绑定 onChange 的时候却不会触发事件!所以,onInput 和 onChange 在这里是有区别的!


emitChange


这里的 onChange, onInput 等回调事件其实都是调用了 emitChange 函数:


emitChange = (originalEvt: React.SyntheticEvent<any>) => {
    const el = this.getEl();
    if (!el) return;
    const html = el.innerHTML;
    if (this.props.onChange && html !== this.lastHtml) {
      // Clone event with Object.assign to avoid
      // "Cannot assign to read only property 'target' of object"
      const evt = Object.assign({}, originalEvt, {
        target: {
          value: html
        }
      });
      this.props.onChange(evt);
    }
    this.lastHtml = html;
  }


这里也很好理解,毕竟只是获取 innerHTML 并构造一个 event,再放到 onChange 里就完事了。简单。


componentDidUpdate


说实话上面的事件我自己也都实现了一次,但是有个问题我一直做不了,那就是我每次输入的时候,光标都会移到最前面!!!比如我输入 "hello",结果就会显示:"olleh",这是什么鬼?!



image.png


用法:


function App() {
  const [value, setValue] = useState('');
  const onChange = (value: string) => {
    console.log(value);
    setValue(value)
  }
  return (
    <div className="App">
      <VarInput value={value} onInput={onChange} />
    </div>
  );
}


这个是因为我在 setValue 的时候,光标会移到最前面,回到源码,它也是考虑到了这一点的。他用了一个函数放在 componentDidUpdate 里处理了这种情况:


componentDidUpdate() {
  const el = this.getEl();
  if (!el) return;
  // Perhaps React (whose VDOM gets outdated because we often prevent
  // rerendering) did not update the DOM. So we update it manually now.
  if (this.props.html !== el.innerHTML) {
    el.innerHTML = this.props.html;
  }
  this.lastHtml = this.props.html;
  replaceCaret(el);
}
function replaceCaret(el: HTMLElement) {
  // Place the caret at the end of the element
  const target = document.createTextNode('');
  el.appendChild(target);
  // do not move caret if element was not focused
  const isTargetFocused = document.activeElement === el;
  if (target !== null && target.nodeValue !== null && isTargetFocused) {
    var sel = window.getSelection();
    if (sel !== null) {
      var range = document.createRange();
      range.setStart(target, target.nodeValue.length);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    }
    if (el instanceof HTMLElement) el.focus();
  }
}


这个函数确保了每次更新后光标都会移动到最后一个位置上,果然我想的还是太 naive 了。


shouldComponentUpdate


最后一个部分就是 shouldComponentUpdate 了,这里主要是做一些 props 是否改变来判断是否需要重新渲染组件而已,相信大家都会就不多做介绍了。


最后




下次秀这个属性的时候可以把这篇文章也给妹子看看,一起学习🐶(逃

(完)



相关文章
|
JavaScript 前端开发
vue element plus Empty 空状态
vue element plus Empty 空状态
349 0
vue element plus Empty 空状态
|
存储 JavaScript 前端开发
好烦,怎么输入拼音的过程也会触发input事件!!!
好烦,怎么输入拼音的过程也会触发input事件!!!
364 0
antd-procomponent中编辑表格动态数据设置的使用
antd-procomponent中编辑表格动态数据设置的使用
508 0
|
测试技术
用navigator.sendBeacon完成网页埋点异步请求记录用户行为,当网页关闭的时候,依然后完美完成接口请求,不会因为浏览器关闭了被中断请求。
用navigator.sendBeacon完成网页埋点异步请求记录用户行为,当网页关闭的时候,依然后完美完成接口请求,不会因为浏览器关闭了被中断请求。
|
安全 大数据 测试技术
Mongodb亿级数据量的性能测试比较完整收藏一下
原文地址:http://www.cnblogs.com/lovecindywang/archive/2011/03/02/1969324.html 进行了一下Mongodb亿级数据量的性能测试,分别测试如下几个项目: (所有插入都是单线程进行,所有读取都是多线程进行) 1) 普通插入性能 (插...
4383 0
|
NoSQL 索引
MongoDB查询优化:从 10s 到 10ms
本文是我前同事付秋雷最近遇到到一个关于MongoDB执行计划选择的问题,非常有意思,在探索源码之后,他将整个问题搞明白并整理分享出来。付秋雷(他的博客)曾是Tair(阿里内部用得非常官方的KV存储系统)的核心开发,目前就职于蘑菇街。
|
存储 缓存 中间件
|
缓存 前端开发
keep-alive缓存三级及三级以上路由
keep-alive缓存三级及三级以上路由
500 0
|
存储 JavaScript 前端开发
【JavaScript】JavaScript 中的 Class 类:全面解析
【JavaScript】JavaScript 中的 Class 类:全面解析
580 1
|
缓存 网络协议 开发者
HTTP1.0、HTTP1.1 、HTTP2.0和HTTP3.0 的区别【面试题】
HTTP1.0、HTTP1.1 、HTTP2.0和HTTP3.0 的区别【面试题】
1672 0
HTTP1.0、HTTP1.1 、HTTP2.0和HTTP3.0 的区别【面试题】