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

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

最初调研的 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 (
    <div>
      <p data-id={id} onClick={this.haddleClick} className="panelName">
        <i className="iconfont icon-folder-closed"></i>
        {name}
      </p>
      <div className={divClass}>
        {panel}
      </div>
    </div> 
    )
}

针对菜单栏添加 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 (
        <div className={activeClass} onClick={()=>{
          setActiveId(fileId)
        }}>
          <i className="iconfont icon-file"></i>
          <ContextMenuProvider className="provider" id="menu_id" >
           <input className="inputClass" data-id={fileId} type="text" value={this.state.value} onChange={this.handleChange.bind(this)} disabled="disabled"/>
          </ContextMenuProvider>
        </div>
      )
    }
    

    再回到 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();
    }   
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容