菜單欄中子文件顯示/隱藏的切換動(dòng)畫(huà)
最初調(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ì)開(kāi)發(fā)時(shí)的限制減少了很多限番。但是在實(shí)際使用中發(fā)現(xiàn)了另一個(gè)比較重要的問(wèn)題:這些 wrapper 都有一個(gè)屬性isOpened來(lái)控制當(dāng)前組件是展開(kāi)還是折疊狀態(tài),這由我們傳入 props 控制弥虐,而當(dāng)切換展示文件時(shí)(也就是改變了model 中的 activeId)就會(huì)觸發(fā)該 wrapper 的rerender,即如果該組件原本是展開(kāi)的驴剔,那么切換展示文件之后丧失,該組件就會(huì)出現(xiàn)由先折疊(默認(rèn)狀態(tài))轉(zhuǎn)為展開(kāi)(props 使然)的動(dòng)畫(huà)。
在當(dāng)前的場(chǎng)景(展示多級(jí)文件目錄)下琳拭,動(dòng)畫(huà)依靠數(shù)據(jù)/狀態(tài)驅(qū)動(dòng)描验,當(dāng)前打開(kāi)的文件即 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)畫(huà)過(guò)程)而這是我們不希望看到的揖赴。
此時(shí)想要 jquery 時(shí)代的控制:只有在我點(diǎn)擊文件夾的時(shí)候才進(jìn)行展開(kāi)/折疊動(dòng)畫(huà)的過(guò)程切換,其余 rerender 的時(shí)候不應(yīng)用動(dòng)畫(huà)效果渐北。同時(shí)點(diǎn)擊之后展開(kāi)/折疊的狀態(tài)還需要在 props 中去更新铭拧,當(dāng)切換文件時(shí)不至于使得原本打開(kāi)的文件夾被折疊上恃锉。此時(shí)動(dòng)畫(huà)就需要自己使用 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 一直沒(méi)變,添加的 class 也不變谷异,所以不會(huì)有動(dòng)畫(huà)過(guò)程出現(xiàn)锦聊。所以在整體 rerender 的過(guò)程中,如果想要保證內(nèi)部組件的動(dòng)畫(huà)過(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-contextmenu
和react-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)聽(tīng)事件,切記使用完成后移除莫绣。
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 }) } }) }
- 執(zhí)行
-
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(); }