打造在線編譯器 之 對(duì)文件目錄的操作

菜單欄中子文件顯示/隱藏的切換動(dòng)畫

最初調(diào)研的 rc-collapse 組件,但是其 Collapse 與 Panel 的設(shè)置并不適合于文件目錄結(jié)構(gòu)的展示,并且這兩者父子組件耦合嚴(yán)重,便轉(zhuǎn)而調(diào)研單純的 Collapse組件,比如react-collapse。這是單純的一個(gè) component-wrapper for collapse animation,在實(shí)現(xiàn)目錄結(jié)構(gòu)的展示上對(duì)開發(fā)時(shí)的限制減少了很多。但是在實(shí)際使用中發(fā)現(xiàn)了另一個(gè)比較重要的問題:這些 wrapper 都有一個(gè)屬性isOpened來(lái)控制當(dāng)前組件是展開還是折疊狀態(tài),這由我們傳入 props 控制,而當(dāng)切換展示文件時(shí)(也就是改變了model 中的 activeId)就會(huì)觸發(fā)該 wrapper 的rerender,即如果該組件原本是展開的,那么切換展示文件之后,該組件就會(huì)出現(xiàn)由先折疊(默認(rèn)狀態(tài))轉(zhuǎn)為展開(props 使然)的動(dòng)畫。

在當(dāng)前的場(chǎng)景(展示多級(jí)文件目錄)下,動(dòng)畫依靠數(shù)據(jù)/狀態(tài)驅(qū)動(dòng),當(dāng)前打開的文件即 activeItem,是根據(jù)當(dāng)前頁(yè)面狀態(tài)中的 activeId ===item.id? 來(lái)添加 active 的樣式的。那么在多級(jí)菜單欄中,切換文件之后,activeId 改變, activeItem 也必然改變,此時(shí)整個(gè)目錄是在做 diff 比較然后刷新的,那么涉及到文件夾的顯示/隱藏必然也將重新渲染(如果文件 isOpened狀態(tài)保存在每個(gè) Collapse 組件內(nèi)部,則 rerender 之后都會(huì)是恢復(fù) state的初始值,如果放在 props 則必然會(huì)顯示組件重新渲染的動(dòng)畫過(guò)程)而這是我們不希望看到的。

此時(shí)想要 jquery 時(shí)代的控制:只有在我點(diǎn)擊文件夾的時(shí)候才進(jìn)行展開/折疊動(dòng)畫的過(guò)程切換,其余 rerender 的時(shí)候不應(yīng)用動(dòng)畫效果。同時(shí)點(diǎn)擊之后展開/折疊的狀態(tài)還需要在 props 中去更新,當(dāng)切換文件時(shí)不至于使得原本打開的文件夾被折疊上。此時(shí)動(dòng)畫就需要自己使用 CSS 控制去實(shí)現(xiàn)就更容易一些,同時(shí)基于 props 記錄管理文件夾的當(dāng)前狀態(tài)。

實(shí)現(xiàn):基于 原生 div 展示 sidebar ,同時(shí)默認(rèn)折疊,當(dāng)點(diǎn)擊文件夾時(shí) 通過(guò) updateProps 更新該文件夾 props.isCollapsed 的 值,進(jìn)而觸發(fā)對(duì) class 進(jìn)行修改,實(shí)現(xiàn)折疊/顯示的切換。當(dāng)切換激活文件時(shí),整個(gè)sideMenu 仍然會(huì)rerender, 但因?yàn)?props.isCollapsed 一直沒變,添加的 class 也不變,所以不會(huì)有動(dòng)畫過(guò)程出現(xiàn)。所以在整體 rerender 的過(guò)程中,如果想要保證內(nèi)部組件的動(dòng)畫過(guò)程在 rerender 時(shí)不出現(xiàn),自行控制 css 是不錯(cuò)的方法。

其中記錄各個(gè)文件夾的props.isCollapsed狀態(tài)由 model 中一個(gè)對(duì)象記錄各個(gè)文件夾的狀態(tài)

collapseObj={
    dirId1:true,
    dirId2:false
}

對(duì)于每個(gè)文件夾結(jié)構(gòu)獨(dú)立為一個(gè)組件(代碼有刪改):

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> 
    )
}

針對(duì)菜單欄添加 contextMenu 如新建文件/重命名/刪除文件等操作。

js 支持右鍵自定義事件contextMenu,但是自己實(shí)現(xiàn)時(shí)需要封裝好一些功能,其中最重要的是不論點(diǎn)擊rename/createFile/deleteFile 哪個(gè)按鈕,我們都需要得到觸發(fā)該 contextMenu 的元素id。調(diào)研的有react-contextmenureact-contexify,盡管后者 star數(shù)量上比較少,但更能滿足我們的需求,因?yàn)樵诋?dāng)前場(chǎng)景(展示多級(jí)目錄)下,我們需要簡(jiǎn)單的得到觸發(fā) contextMenu 的元素,前者對(duì)此的支持度并不好。

react-contexify封裝在 Item 上的click方法會(huì)接受3個(gè)參數(shù)handleClick(targetNode,ref,data)。得到觸發(fā)該 contextMenu 的元素targetNode之后,我們?nèi)绾蔚玫狡?id 屬性呢,此處不要忘了威力無(wú)窮的屬性data-xxx,可以給 targetNode 添加data-id屬性,然后通過(guò)targetNode.dataset.id得到。

對(duì)文件的 delete/rename/create 操作,我們由易到難來(lái)介紹:

  • deleteOperation:對(duì)于刪除操作,在前端我們比較容易得到將要?jiǎng)h除的文件的 id,直接提交即可,如果要在 model 中處理的話,記得這是一個(gè)多級(jí)的文件目錄結(jié)構(gòu)來(lái)說(shuō),各種處理都要進(jìn)行深度(拷貝/過(guò)濾)
  • renameOperation:這是一個(gè)副作用比較多的操作,觸發(fā) rename 之后應(yīng)該該文件名可編輯,且其初始內(nèi)容為點(diǎn)擊之前的展示內(nèi)容,進(jìn)行修改之后,回車鍵觸發(fā)內(nèi)容提交,文件名更新,退出可編輯狀態(tài)。如果是esc鍵或者點(diǎn)擊了輸入框之外的區(qū)域,默認(rèn)是撤銷修改,退出可編輯狀態(tài),文件名仍顯然之前狀態(tài)。

    對(duì)于能夠不斷切換是否可編輯狀態(tài)的元素,在這兒使用 input 再何時(shí)不過(guò),其初始不可編輯 disabled,當(dāng)觸發(fā) rename之后改變其 disabled=false。我們都知道input 之類的 form 表單相關(guān)組件在 react 中不同于其他組件,我們要使用受控組件實(shí)現(xiàn)組件顯示與用戶輸入的實(shí)時(shí)交互,那么將每個(gè)文件名(包含 input的組件)獨(dú)立為一個(gè)組件,在組件內(nèi)部通過(guò) state 實(shí)現(xiàn)對(duì) 當(dāng)前input組件的控制。 在此考慮下ContextMenuProvider(react-contexify提供的觸發(fā) contextMenu 的容器)包裹在哪個(gè)元素上比較合適?每個(gè)文件名的 DOM 結(jié)構(gòu)如:('div',{'i','input'})。因?yàn)镃ontextMenuItem 的 onClick 事件是可以直接得到 targetNode 的,在副作用很多的地方如果我們可以直接與 input 交互是很方便的,所以文件名組件主要結(jié)構(gòu)如下:

    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操作的交互過(guò)程,控制 input 編輯狀態(tài)與退出編輯狀態(tài)后的顯示。注意三點(diǎn):

    • 執(zhí)行targetNode.blur()方法后也會(huì)觸發(fā)已注冊(cè)的事件'blur',所以 blur 之后的副作用都放在blur 事件中處理。
    • 在blur 事件中,在處理完之后需要將判斷條件invalidEditing置為非,否則在 blur事件完成之前該段代碼可能會(huì)執(zhí)行隨機(jī)n次。
      • 在 onClick 中添加的監(jiān)聽事件,切記使用完成后移除。
    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 后,向其數(shù)組中插入(unshift)一項(xiàng) 默認(rèn)數(shù)據(jù)。因?yàn)?model 的改變此時(shí)菜單欄會(huì)刷新。對(duì)于創(chuàng)建操作,我們還想要實(shí)現(xiàn):對(duì)該文件名直接進(jìn)入編輯模式,此后就和 renameOperation相同了,只要得到相應(yīng)的 targetNode 觸發(fā)renameOperation 方法就好。那么在數(shù)據(jù)驅(qū)動(dòng)的應(yīng)用中,我們?nèi)绾螌?shí)現(xiàn)這后續(xù)的銜接?--基于 react 的生命周期方法。

    在菜單欄 rerender 完成之后一定會(huì)觸發(fā) componentDidUpdate方法。但是componentDidUpdate方法在很多情況下都會(huì)被觸發(fā),我們需要一個(gè)變量來(lái)判斷只有是 createOperation 導(dǎo)致的更新才執(zhí)行一下操作,并且在完成任務(wù)之后將該變量置非:

    if (this.props.getFileNameIsCreating) {
        let {getActiveId} =this.props
        let untitledNode = document.querySelector(`input[data-id="${getActiveId}"]`);
        this.renameFile(untitledNode)
        this.props.closeCreatingFileNameState();
    }   
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評(píng)論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評(píng)論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評(píng)論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評(píng)論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評(píng)論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評(píng)論 0 291
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評(píng)論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評(píng)論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評(píng)論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評(píng)論 1 296
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,492評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容

  • 深入JSX date:20170412筆記原文其實(shí)JSX是React.createElement(componen...
    gaoer1938閱讀 8,099評(píng)論 2 35
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,836評(píng)論 18 139
  • 自己最近的項(xiàng)目是基于react的,于是讀了一遍react的文檔,做了一些記錄(除了REFERENCE部分還沒開始讀...
    潘逸飛閱讀 3,455評(píng)論 1 10
  • 最近看了一本關(guān)于學(xué)習(xí)方法論的書,強(qiáng)調(diào)了記筆記和堅(jiān)持的重要性。這幾天也剛好在學(xué)習(xí)React,所以我打算每天堅(jiān)持一篇R...
    gaoer1938閱讀 1,712評(píng)論 0 5
  • 五一檔《喜歡·你》的出現(xiàn), 向那些唱衰“霸道總裁愛上我”的影迷啪啪打臉并證明, 只要拍的好, 瑪麗蘇套路照樣可以成...
    大柱哥哥閱讀 694評(píng)論 0 2