打造在线编译器 之 对文件目录的操作

菜单栏中子文件显示/隐藏的切换动画

最初调研的 rc-collapse 组件,但是其 Collapse 与 Panel 的设置并不适合于文件目录结构的展示,并且这两者父子组件耦合严重,便转而调研单纯的 Collapse组件,比如react-collapse。这是单纯的一个 component-wrapper for collapse animation,在实现目录结构的展示上对开发时的限制减少了很多。但是在实际使用中发现了另一个比较重要的问题:这些 wrapper 都有一个属性isOpened来控制当前组件是展开还是折叠状态,这由我们传入 props 控制,而当切换展示文件时(也就是改变了model 中的 activeId)就会触发该 wrapper 的rerender,即如果该组件原本是展开的,那么切换展示文件之后,该组件就会出现由先折叠(默认状态)转为展开(props 使然)的动画。

在当前的场景(展示多级文件目录)下,动画依靠数据/状态驱动,当前打开的文件即 activeItem,是根据当前页面状态中的 activeId ===item.id? 来添加 active 的样式的。那么在多级菜单栏中,切换文件之后,activeId 改变, activeItem 也必然改变,此时整个目录是在做 diff 比较然后刷新的,那么涉及到文件夹的显示/隐藏必然也将重新渲染(如果文件 isOpened状态保存在每个 Collapse 组件内部,则 rerender 之后都会是恢复 state的初始值,如果放在 props 则必然会显示组件重新渲染的动画过程)而这是我们不希望看到的。

此时想要 jquery 时代的控制:只有在我点击文件夹的时候才进行展开/折叠动画的过程切换,其余 rerender 的时候不应用动画效果。同时点击之后展开/折叠的状态还需要在 props 中去更新,当切换文件时不至于使得原本打开的文件夹被折叠上。此时动画就需要自己使用 CSS 控制去实现就更容易一些,同时基于 props 记录管理文件夹的当前状态。

实现:基于 原生 div 展示 sidebar ,同时默认折叠,当点击文件夹时 通过 updateProps 更新该文件夹 props.isCollapsed 的 值,进而触发对 class 进行修改,实现折叠/显示的切换。当切换激活文件时,整个sideMenu 仍然会rerender, 但因为 props.isCollapsed 一直没变,添加的 class 也不变,所以不会有动画过程出现。所以在整体 rerender 的过程中,如果想要保证内部组件的动画过程在 rerender 时不出现,自行控制 css 是不错的方法。

其中记录各个文件夹的props.isCollapsed状态由 model 中一个对象记录各个文件夹的状态

collapseObj={
    dirId1:true,
    dirId2:false
}

对于每个文件夹结构独立为一个组件(代码有删改):

haddleClick=()=>{
  this.props.updateCollapseObj({
    id:this.props.id,
    state:this.props.getCollapseObj[this.props.id]?false:true
  })
}
render(){
  const {id,name,panel}=this.props;
  let divClass= classNames({
    'panel':true,
    'show':this.props.getCollapseObj[this.props.id]
  })

  return (
    

{name}

{panel}
) }

针对菜单栏添加 contextMenu 如新建文件/重命名/删除文件等操作。

js 支持右键自定义事件contextMenu,但是自己实现时需要封装好一些功能,其中最重要的是不论点击rename/createFile/deleteFile 哪个按钮,我们都需要得到触发该 contextMenu 的元素id。调研的有react-contextmenureact-contexify,尽管后者 star数量上比较少,但更能满足我们的需求,因为在当前场景(展示多级目录)下,我们需要简单的得到触发 contextMenu 的元素,前者对此的支持度并不好。

react-contexify封装在 Item 上的click方法会接受3个参数handleClick(targetNode,ref,data)。得到触发该 contextMenu 的元素targetNode之后,我们如何得到其 id 属性呢,此处不要忘了威力无穷的属性data-xxx,可以给 targetNode 添加data-id属性,然后通过targetNode.dataset.id得到。

对文件的 delete/rename/create 操作,我们由易到难来介绍:

  • deleteOperation:对于删除操作,在前端我们比较容易得到将要删除的文件的 id,直接提交即可,如果要在 model 中处理的话,记得这是一个多级的文件目录结构来说,各种处理都要进行深度(拷贝/过滤)
  • renameOperation:这是一个副作用比较多的操作,触发 rename 之后应该该文件名可编辑,且其初始内容为点击之前的展示内容,进行修改之后,回车键触发内容提交,文件名更新,退出可编辑状态。如果是esc键或者点击了输入框之外的区域,默认是撤销修改,退出可编辑状态,文件名仍显然之前状态。

    对于能够不断切换是否可编辑状态的元素,在这儿使用 input 再何时不过,其初始不可编辑 disabled,当触发 rename之后改变其 disabled=false。我们都知道input 之类的 form 表单相关组件在 react 中不同于其他组件,我们要使用受控组件实现组件显示与用户输入的实时交互,那么将每个文件名(包含 input的组件)独立为一个组件,在组件内部通过 state 实现对 当前input组件的控制。 在此考虑下ContextMenuProvider(react-contexify提供的触发 contextMenu 的容器)包裹在哪个元素上比较合适?每个文件名的 DOM 结构如:('div',{'i','input'})。因为ContextMenuItem 的 onClick 事件是可以直接得到 targetNode 的,在副作用很多的地方如果我们可以直接与 input 交互是很方便的,所以文件名组件主要结构如下:

    render(){
      const {fileId, setActiveId} = this.props;
      let activeClass=classNames({
        'list-item':true,
        'active':this.isCurrentFile(fileId)
      })
    
      return (
        
    { setActiveId(fileId) }}>
    ) }

    再回到 rename操作的交互过程,控制 input 编辑状态与退出编辑状态后的显示。注意三点:

    • 执行targetNode.blur()方法后也会触发已注册的事件'blur',所以 blur 之后的副作用都放在blur 事件中处理。
    • 在blur 事件中,在处理完之后需要将判断条件invalidEditing置为非,否则在 blur事件完成之前该段代码可能会执行随机n次。
      • 在 onClick 中添加的监听事件,切记使用完成后移除。
    renameFile(targetNode, ref, data){
      const targetId = targetNode.dataset.id;
      let {renameOperation} =this.props
      const ESCAPE_KEY = 27;
      const ENTER_KEY = 13;
      let invalidEditing=true
      let prevText=targetNode.value;
      targetNode.disabled=false
      targetNode.spellcheck = false;
      targetNode.focus()
      targetNode.addEventListener('blur',function blurHandler(e){
        if (invalidEditing) {
          targetNode.value=prevText;
        }
        targetNode.disabled=true
        invalidEditing=false
        targetNode.removeEventListener('blur',blurHandler,false);
      })
      targetNode.addEventListener('keydown',function keydownHandler(e){
        if (e.which===ESCAPE_KEY) {
          targetNode.blur()
        }else if(e.which===ENTER_KEY){
          invalidEditing=false;
          let newName=targetNode.value
          targetNode.blur()
          targetNode.removeEventListener('keydown',keydownHandler,false);
          if(newName==prevText){
            return
          }
          //model 方法,提交更改信息
          renameOperation({
            id:targetId,
            name:newName
          })
        }
      })
    }
    
  • createOperation : 得到parentId 后,向其数组中插入(unshift)一项 默认数据。因为 model 的改变此时菜单栏会刷新。对于创建操作,我们还想要实现:对该文件名直接进入编辑模式,此后就和 renameOperation相同了,只要得到相应的 targetNode 触发renameOperation 方法就好。那么在数据驱动的应用中,我们如何实现这后续的衔接?--基于 react 的生命周期方法。

    在菜单栏 rerender 完成之后一定会触发 componentDidUpdate方法。但是componentDidUpdate方法在很多情况下都会被触发,我们需要一个变量来判断只有是 createOperation 导致的更新才执行一下操作,并且在完成任务之后将该变量置非:

    if (this.props.getFileNameIsCreating) {
        let {getActiveId} =this.props
        let untitledNode = document.querySelector(`input[data-id="${getActiveId}"]`);
        this.renameFile(untitledNode)
        this.props.closeCreatingFileNameState();
    }   
    

你可能感兴趣的:(打造在线编译器 之 对文件目录的操作)