這是我幾個月以前的項目做品,花了至關的時間去完善。博客人氣不高,但拿代碼的人很多,因此一直處於保密狀態。沒有公開代碼。但若是對你有幫助,並能提出指導意見的,我將十分感謝。javascript
參考設計稿實現一個簡單的我的任務管理系統:以下圖css
任務需求描述:html
默認分類
,進入頁面時默認選中默認分類
。prompt
。當鼠標hover
過某一個分類時,右側會出現刪除按鈕,點擊後,彈出確認是否刪除的浮層,確認後刪除掉該分類。彈出的確認浮層能夠自行設計實現,也能夠直接使用confirm
。不能爲默認分類
添加子分類,也不能刪除默認分類
。已完成
或未完成
。新增任務
的按鈕,點擊後,右側列會變成新增任務編輯界面。已完成
或者未完成
的任務。完成任務
的操做按鈕及編輯任務
的按鈕。完成任務
按鈕時,彈出確認是否確認完成的浮層,確認後該任務完成,更新中間列任務的狀態。彈出的確認浮層能夠自行設計實現,也能夠直接使用confirm
。編輯任務
操做後,右側變動爲編輯窗口。任務實現要求:前端
注意java
該設計稿僅爲線框原型示意圖,全部的視覺設計不須要嚴格按照示意圖。若是有設計能力的同窗,歡迎實現得更加美觀,若是沒有,也能夠按照線框圖實現。如下內容能夠自行發揮:git
整個環境應該經過後端的交互實現。可是簡單地實現就是ajax方法。github
項目要求不用任何類庫框架,可是任務2中的$d
類庫是本身寫的。能夠檢驗$d
類庫的可靠性,因此用了也問題不大。ajax
待辦事項列表是一個至關典型的數據結構,再設計數據結構時,顯然應該用面向對象的思路觸發操做。spring
第一個問題就是高度自填充。json
和寬度同樣,一個元素要在父級高度有數值時才能設定百分比高度。
分類列表的方法應該是ul-li體系
- ul.classify-list
- li
- h3.title-list
- a.title1:點擊標籤,包含分類一級標題,點擊時給h3加上激活樣式。
- a.close:關閉刪除按鈕(正常時隱藏,鼠標劃過期顯示)
- ul classify-list2
- li (如下是二級分類標題結構)
其中特殊分類是「默認分類」,不能刪除
點擊標題出現激活樣式:
我以爲這隻須要考慮當前點選邏輯,當點擊了二級分類,再點擊其它一級分類時,激活樣式顯示在所點擊的一級分類上。原來的二級分類激活樣式消失。
$('.title1').on('click',function(){ $('.title-list').removeClass('classify-active'); $(this.parentNode).addClass('classify-active'); }); $('.title2').on('click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list',this.parentNode.parentNode.parentNode.parentNode).addClass('classify-active'); $('.title-list2').removeClass('classify-active2'); $(this.parentNode).addClass('classify-active2'); });
注:兩次點擊的效果不一樣,因此考慮寫一個toggle方法。
//toggle方法: $d.prototype.toggle=function(_event){ var _arguments=Array.prototype.slice.call(arguments).slice(1,arguments.length);//把toggle的arguments轉化爲數組存起來,以便在其它函數中能夠調用。 //console.log(_arguments); //私有計數器,計數器會被一組對象所享用。 function addToggle(obj){ var count=0; addEvent(obj,_event,function(){ _arguments[count++%_arguments.length].call(obj); }); } each(this.objs,function(item,index){ addToggle(item); }); }; //使用示例: $('.title1').toggle('click',function(){ $('.classify-list2',this.parentNode.parentNode).obj.style.display='block'; },function(){ $('.classify-list2',this.parentNode.parentNode).obj.style.display='none'; });
而後再寫一個hover方法
//hover方法 $d.prototype.hover=function(fnover,fnout){ var i=0; //對於返回器數組的內容 each(this.objs,function(item,index){ addEvent(item,'mouseover',fnover); addEvent(item,'mouseout',fnout); }); return this; }; //使用示例 $('.title-list').hover(function(){ if($('.classify-close',this.parentNode).obj){ $('.classify-close',this.parentNode).move({'opacity':100}); } },function(){ if($('.classify-close',this.parentNode).obj){ $('.classify-close',this.parentNode).move({'opacity':0}); } });
還有一個狀態,若是點擊某個分類,下面沒有子分類,就什麼都不顯示
$('.title1').toggle('click',function(){ if($('.classify-list2',this.parentNode.parentNode).obj){ $('.classify-list2',this.parentNode.parentNode).obj.style.display='block'; } },function(){ if($('.classify-list2',this.parentNode.parentNode).obj){ $('.classify-list2',this.parentNode.parentNode).obj.style.display='none'; } });
基本邏輯以下
篩選欄有三個按鈕和一個搜索框,其中,這三個按鈕應該擁有激活狀態
$('.todo-btn').on('click',function(){ $('.todo-btn').removeClass('todo-btn-active'); $(this).addClass('todo-btn-active'); });
後面的基本結構是這樣的——已完成和未完成都應該以不一樣的樣式顯示
<div class="todo-content"> <ul class="todo-date"> <span>2017-1-24</span> <li class="completed"><a href="javascript:;">任務1</a></li> <li class="uncompleted"><a href="javascript:;">任務2</a></li> <li class="completed"><a href="javascript:;">任務3</a></li> </ul> <ul class="todo-date"> <span>2017-1-25</span> <li class="completed"><a href="javascript:;">任務1</a></li> <li class="completed"><a href="javascript:;">任務2</a></li> <li class="uncompleted"><a href="javascript:;">任務3</a></li> </ul> </div>
界面大體是這個樣子
要求篩選欄經過keyUp事件輸入或點擊按鈕,下面的框動態顯示結果。
這些交互是經過數據特性來設置的,因此不必在這裏寫。
相似Ps畫板。注意畫板去容許出現垂直滾動條。
<div class="content"> <div class="content-outer"> <div class="content-info"> <div class="content-header"> <h3>待辦事項標題</h3> <a href="javascript:;">編輯</a> </div> <div class="content-substract"> 任務日期:2017-1-25 </div> </div> <div class="content-content"> <div class="content-paper"> <h4>啊!今天是個好日子</h4> <p>完成task3的設計和樣式實現。</p> </div> </div> </div> </div>
佈局樣式
.content{ width: auto; height: inherit; padding-left: 512px; } .content-outer{ height: 100%; position: relative; } .content-info{ height: 91px; } .content-content{ position: absolute; width: 100%; top:91px; bottom: 0; background: #402516; overflow-y: scroll; }
利用絕對定位的方式實現畫板區(.content-content
)的高度自適應,而後.paper
經過固定的margin實現區域延伸。
那麼整個界面就出來了。
嚴格點說說「前端組件開發」這個名字並不許確。這裏只涉及了本項目中組件的控制邏輯,並不展現數據結構部分的邏輯。
給分類列表和任務欄添加一個「添加」按鈕,要求添加時彈出一個模態彈窗。
彈窗提供最基本的功能是:一個輸入框,自定義你的分類名或任務名,一個取消按鈕,一個肯定按按鈕。
模態彈窗是由兩個部分組成
採用的是動態建立的方式能夠給指定的彈窗添加id,兩個都是用絕對定位實現。
<div class="add-mask"></div> <div id="(自定義)" class="add"> <div class="add-title"> <h4>添加內容</h4> </div> <div class="add-content"> <span>名稱:</span> <input type="text" name="" value=""> <div class="btns"> <button id="exit" type="button">取消</button> <button id="submit" type="button">肯定</button> </div> </div> </div>
這個應該直接放到body標籤結束前。
寫一個面向對象的組件,能夠想象它的調用過程是怎樣的:
// 以添加分類爲例: var addCategoryModal=new Modal(); // 初始化 addCategoryModal.init({ //這裏放配置 }); // 生成窗口 categoryModal.create();
new 出一個新的組件,而後進行初始化,傳入必要的參數,若是不傳配置,組件有自身的配置。
function Modal(){ this.settings={ // 這裏放默認的配置 }; }
轉入的配置疊加能夠經過一個擴展函數
來實現:
function extend(obj1,obj2){ for(var attr in obj2){ obj1[attr]=obj2[attr]; } } // ... //這裏是以自定義的option配置覆蓋內部配置 Modal.prototype.init=function(option){ extend(this.settings,option); };
那麼這個框架就搭建起來了。
彈窗須要哪些配置?
在這個項目中,只須要指定彈窗提示內容title
和彈窗類型type
(這裏就三個,一個是目錄addCtategory
,另外一個是任務addMission
,最後一個是通用提示框tips)就能夠了。
其中,type將成爲模態彈窗頂層容器的id值。
生成窗口無非是給DOM追加一個節點。
Modal.prototype.create=function(){ var oDialog=document.createElement('div'); oDialog.className='add'; oDialog.id=this.settings.type; if(this.settings.type=='tips'){ oDialog.innerHTML = '<div class="add-title">'+ '<h4>信息提示</h4>'+ '</div>'+ '<div class="add-content">'+ '<span>'+this.settings.tips+'</span>'+ '<div class="btns">'+ '<button id="exit" type="button">我知道了</button>'+ '</div>'+ '</div>'; }else{ oDialog.innerHTML = '<div class="add-title">'+ '<h4>添加內容</h4>'+ '</div>'+ '<div class="add-content">'+ '<span>'+this.settings.title+'名稱:</span>'+ '<input class="input" type="text" value="">'+ '<div class="btns">'+ '<button id="exit" type="button">取消</button>'+ '<button class="submit" type="button">肯定</button>'+ '</div>'+ '</div>'; } // 顯示效果 document.body.appendChild(oDialog); $('.add-mask').obj.style.display='block'; //彈窗位置指定,絕對居中 var clientWidth=document.documentElement.clientWidth; var clientHeight=document.documentElement.clientHeight; oDialog.style.left=(clientWidth)/2-175+'px'; oDialog.style.top=(clientHeight)/2-75+'px'; //關閉按鈕 function remove(){ document.body.removeChild(oDialog); $('.add-mask').obj.style.display='none'; $(this).un('click',remove); } $('#exit').on('click',remove); };
好了。咱們給一個#addCategory的按鈕添加點擊事件:
$('#addCategory').on('click',function(){ var categoryModal=new Modal(); categoryModal.init( { type:newCategory, title:'目錄' } ); categoryModal.create(); });
效果就出來了:
要讓這個組件具備基本的功能,還須要寫遮罩層,取消按鈕等。
注意:如下效果所有在create方法中完成
遮罩(.mask):遮罩是一個隱藏的,不須要動態顯示。
.add-mask{ position: absolute; left: 0; top:0; right: 0; bottom:0; background: rgba(0,0,0,0.5); z-index: 99;/*注意.add的層級應該大於99*/ }
<div class="add-mask" style="display:none;"></div>
而後添加一個顯示效果:
// 顯示效果 document.body.appendChild(oDialog); $('.add-mask').obj.style.display='block';
本着清理乾淨的精神,除了把oDialog
從document中清掉。
//關閉按鈕 function remove(){ document.body.removeChild(oDialog); $('.add-mask').obj.style.display='none'; } $('#exit').on('click',remove);
那麼取消就寫完了。
肯定按鈕也能夠寫一個手動關閉彈窗的方法:
Modal.prototype.exit=function(){ document.body.removeChild($('.add').obj); $('.add-mask').obj.style.display='none'; }
實際上
到此能夠認爲,這個靜態的模態彈窗完成。
效果:
雖然任務要求不用任何框架,可是咱們的需求在當前來講已經開始超越了任務自己的需求,不用jQuery勉強能夠接受,可是前端渲染你的content部份內容,marke.js顯然是最好的選擇。關於marked.js的用法,能夠參照marked.js簡易手冊。
實際上這已是第三次在項目中用到mark.js,用起來水到渠成。
固然不想作任何處理的話,也能夠跳過這節。
如今把它拖進來。並引用一個基本能搭配當前頁面風格的樣式庫。
<link rel="stylesheet" type="text/css" href="css/css.css"/> <link rel="stylesheet" type="text/css" href="css/solarized-dark.css"/> <script type="text/javascript" src="js/dQuery.js"></script> <script type="text/javascript" src="js/marked.js"></script> <script type="text/javascript" src="js/highlight.pack.js"></script> <script >hljs.initHighlightingOnLoad();</script> <script type="text/javascript" src="js/js.js"></script>
而後:
// 渲染頁面模塊 var rendererMD = new marked.Renderer(); marked.setOptions({ renderer: rendererMD, highlight: function (code,a,c) { return hljs.highlightAuto(code).value; }, gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, smartLists: true, smartypants: false }); //用於測試效果 $('.content-paper').obj.innerHTML=marked('# 完成markdown模塊開發\n---\nRendered by **marked**.\n\n```javascript\nfunction(){\n console.log("Hello!Marked.js!");\n}\n```\n這是響應式圖片測試:\n![](http://images2015.cnblogs.com/blog/1011161/201701/1011161-20170127184909206-861797658.png)\n1\. 傳進去前端的代碼結構必須符合樣式庫的要求。\n2\. 我要把頁面的代碼通通是現貨高亮顯示——好比這樣`alert(Hello!)`');
儘管有了樣式庫的支持,可是這個樣式庫只是定義了配色。而瀏覽器默認的樣式被當初的css-reset給幹掉了。
markdown最經常使用的效果就是代碼高亮,搭配圖片顯示,
在過去的項目(Node.js博客搭建)中,我已經使用了marked.js重寫了一個還算漂亮的樣式庫(基於marked.js樣式庫和bootstrap樣式庫code和pre部分)。如今把重寫CSS的要點簡單概括如左:
.content-paper img{ display: block; max-width: 100%; height: auto; border: 1px solid #ccc; }
.content-paper ul li{ list-style: disc; margin-left: 15px; } .content-paper ol li{ list-style: decimal; margin-left: 15px; }
1.05em
。效果以下:
那麼效果馬上有了。
搜索組件只作一件事情:根據代辦事項列表窗(ul.todo-content
)中的文本節點,經過監聽文本輸入框(input.search
)的內容,綁定keyUp事件綁定,查找數據集。
若是按照封裝對象的思路來寫,一個是監聽模塊,一個是顯示模塊。爲了方便起見,給各自的元素加上同名id。
就實現上來講彷佛很簡單,查找#todo-content
裏面的文本節點,而後轉化爲數組:
// 搜索組件 function Search(listener,shower){ this.listener=$(listener); this.shower=$(shower); } Search.prototype.filter=function(){ var value=this.listener.obj.value; var content=this.shower.obj.innerText; console.log(content.split('\n')); }; $(funciton(){ $('#search').on('keyup',function(){ var search=new Search('#search','#todo-content'); search.filter(); }); });
然而不幸的事情發生了:
竟然把任務日期打出來了。此外還有一個空文本。
由於html代碼結構是這樣的:
<div id='todo-content' class="todo-content"> <ul class="todo-date"> <span>2017-1-24</span> <li class="completed"><a href="javascript:;">任務1</a></li> <li class="uncompleted"><a href="javascript:;">任務2</a></li> <li class="completed"><a href="javascript:;">任務3</a></li> </ul> <ul class="todo-date"> <span>2017-1-25</span> <li class="completed"><a href="javascript:;">任務1</a></li> <li class="completed"><a href="javascript:;">任務2</a></li> <li class="uncompleted"><a href="javascript:;">任務3</a></li> </ul> </div>
既然這樣,就查找var search=new Search('#search','#todo-content li');
把,而後對li對象作一個for循環。沒有的就設置display爲none:
// 搜索組件 function Search(listener,shower){ this.listener=$(listener); this.shower=$(shower); } Search.prototype.filter=function(){ var value=this.listener.obj.value; var content=[]; for(var i=0;i<this.shower.objs.length;i++){ this.shower.objs[i].style.display='block'; content.push(this.shower.objs[i]); if(this.shower.objs[i].innerText.indexOf(value)==-1){ this.shower.objs[i].style.display='none'; } } }; // 調用 var search=new Search('#search','#todo-content li'); $('#search').on('keyup',function(){ search.filter(); });
效果:
目前搜索組件有一個很大的問題,就是沒法實現數據的雙向綁定。
輸入框搜索組件是獨立的判斷條件。下面的三個按鈕是公用一套判斷信息。
思路是活用html元素的data
屬性。給全部節點添加data-search
和data-query
兩個屬性,全部html元素初始的兩個屬性都是true。當不一樣的按鈕被點選,就執行query方法把符合條件的元素的data-xxx
設置爲true。而後再進行渲染render
,兩個屬性都爲true的纔不給添加.hide
樣式(hide的樣式就是display爲none)。
// 搜索組件 function Search(listener,shower){ this.listener=$(listener); this.shower=$(shower); this.key='all'; } Search.prototype.filter=function(){ var value=this.listener.obj.value; // 先所有設置爲true for(var j=0;j<this.shower.objs.length;j++){ this.shower.objs[j].setAttribute('data-search', "true"); } //綁定當前按鈕的搜索條件 this.query(this.key); for(var i=0;i<this.shower.objs.length;i++){ if(this.shower.objs[i].innerText.indexOf(value)==-1){ this.shower.objs[i].setAttribute('data-search', 'false'); } } this.renderer(); }; Search.prototype.query=function(key){ this.key=key; for(var j=0;j<this.shower.objs.length;j++){ //this.shower.objs[i].style.display='block'; this.shower.objs[j].setAttribute('data-key',"true"); } this.renderer(); for(var i=0;i<this.shower.objs.length;i++){ this.shower.objs[i].setAttribute('data-key',"true"); if(key!=='all'){ if(this.shower.objs[i].className!==key){ this.shower.objs[i].setAttribute('data-key',"false"); } } } this.renderer(); }; // 最後是渲染方法 Search.prototype.renderer=function(){ for(var i=0;i<this.shower.objs.length;i++){ var a=this.shower.objs[i].getAttribute('data-search'); var b=this.shower.objs[i].getAttribute('data-key'); if(a=="true"&&b=="true"){ $(this.shower.objs[i]).removeClass('hide'); }else{ $(this.shower.objs[i]).addClass('hide'); } } };
那麼搜索機制就幾行
var search=new Search('#search','#todo-content li'); $('#search').on('keyup',function(){ search.filter(); }); $('#completed').on('click',function(){ search.query('completed'); }); $('#all').on('click',function(){ search.query('all'); }); $('#uncompleted').on('click',function(){ search.query('uncompleted'); });
最終效果:
數據可視化是個大坑。
基本邏輯是:
顯然用面向對象的思路是最好的。
涉及無級樹的設計。
縱觀前面的邏輯,每一個數據須要哪些特性?
一個好的數據結構,前端拿到以後渲染也是方便的。不妨直觀一點,用數組+對象的方式來組織信息。
var json=[ { "categoryName":一級目錄名, "id":惟一的流水號或是時間戳 "missions"(該目錄下屬的任務):[ { "id":任務id "title":任務名, "createTime":推送時間, "isCompleted":是否完成, "content":任務的文本內容 }, //... ],// 沒有則爲空數組[] "list"(該目錄下屬的直接子分類):[ { "categoryName":二級目錄名, "id":... 。。。 } ]//沒有則爲空數組[]。 }, { "categoryName":一級目錄名2 "mission":[ //... ], "list":[ //... ] }, //... ]
對沒有使用真正後端支持的的前端渲染來講,處理這樣的數據是十分之麻煩的。
接下來就是渲染。
多級分類的ul以下:
構造一個對象:
/*遞歸實現獲取無級樹數據並生成DOM結構*/ function Data(data){ this.data=data; } Data.prototype.renderTree=function(selector){ var _this=this; var result=''; (function getTree(_data){ var obj=_data; for(var i=0;i<obj.length;i++){ var str=''; if(obj==_this.data){//若是是頂層一級標題則用較大的顯示 str= '<li class="lv1"><h3 class="title-list">'+ '<a '+'data-id="'+obj[i]["id"]+'"'+' class="title1" href="javascript:;"><img src="images/dirs.png"> '+obj[i]["categoryName"]+'</a>'+ '<a class="classify-close" href="javascript:;"><img src="images/close.png"></a>'+ '</h3>'; }else{ str='<li>'+ '<h4 class="title-list2">'+ '<a '+'data-id="'+obj[i]["id"]+'"'+' class="title2" href="javascript:;"><img src="images/dir.png" alt=""> '+obj[i]["categoryName"]+'</a>'+ '<a class="classify-close2" href="javascript:;"><img src="images/close.png"></a>'+ '</h4>'; } result+=str; if(obj[i]["list"]!==[]){ //注意:此處表面還未結束 result+='<ul class="classify-list2">'; getTree(obj[i]["list"]); result+='</ul></li>'; }else{ result+='</li>'; } } })(_this.data); $(selector).obj.innerHTML=result; };
好比,我要在ul#categories
下渲染數據:
var _data=new Data(json); _data.renderTree('#categories');
還記得動態交互吧。以前的DOM操做極其噁心(出現了連續4個parentNode),並且是寫死的,如今實現一個根據標籤查找第一個祖先class名的函數:
function getAcient(target,className){ //console.log(target.parentNode.classList); var check=false; for(var i=0;i<target.parentNode.classList.length;i++){ if(target.parentNode.classList[i]==className){ check=true; break; } } if(check){ return target.parentNode; }else{ return getAcient(target.parentNode,className); } } // 好比說,getAcient(document.getElementById('li1'),'ul1') // 表示查找一個#li1的元素最近的、class名包括.ul的祖先。
有了它,以前的噁心寫法大多能夠取代了。
點擊li.lv1下的任何a,都響應內容
$('.lv1').delegate('a','click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list2').removeClass('classify-active2'); // 頂層加類 $('h3',getAcient(this,'lv1')).addClass('classify-active'); if(this.parentNode.className!=="title-list"){ $(this.parentNode).addClass('classify-active2'); } }); $('.title2').on('click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list2').removeClass('classify-active2'); $(this.parentNode).addClass('classify-active2'); });
如今反觀toggle
,添加數據時展現很是不直觀,爲了代碼的簡潔,因此刪掉。
接下來把全部涉及效果的函數封裝爲Data的一個方法,每次執行renderTree()方法,就渲染一次交互效果。
Data.prototype.renderCategoryEfect=function(){ $('.title2').on('click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list2').removeClass('classify-active2'); $(this.parentNode).addClass('classify-active2'); }); // $('.title2').toggle('click',function(){ // if($('.classify-list2',this.parentNode.parentNode).obj){ // $('.classify-list2',this.parentNode.parentNode).obj.style.display='block'; // } // // },function(){ // if($('.classify-list2',this.parentNode.parentNode).obj){ // $('.classify-list2',this.parentNode.parentNode).obj.style.display='none'; // } // }); // $('.title1').toggle('click',function(){ // if($('.classify-list2',this.parentNode.parentNode).obj){ // $('.classify-list2',this.parentNode.parentNode).obj.style.display='block'; // } // },function(){ // if($('.classify-list2',this.parentNode.parentNode).obj){ // $('.classify-list2',this.parentNode.parentNode).obj.style.display='none'; // } // }); $('.title-list2').hover(function(){ if($('.classify-close2',this.parentNode).obj){ $('.classify-close2',this.parentNode).move({'opacity':100}); } },function(){ if($('.classify-close2',this.parentNode).obj){ $('.classify-close2',this.parentNode).move({'opacity':0}); } }); $('.title-list').hover(function(){ if($('.classify-close',this.parentNode).obj){ $('.classify-close',this.parentNode).move({'opacity':100}); } },function(){ if($('.classify-close',this.parentNode).obj){ $('.classify-close',this.parentNode).move({'opacity':0}); } }); };
這裏沒有把delegate
監聽事件寫進去,由於這涉及到其它對象的交互。
通過這一步,至少檯面上的代碼已經大大簡化了。
當點擊添加分類,出來一個模態彈窗,在模態彈窗輸入內容。則添加一個目錄到相應的數據結構下:
固然是push方法.
var obj={ "categoryName":value,//經過輸入框獲取到的數據 "id":Date.parse(new Date()), "missions":[], "list":[] };
這須要id值。
Data.prototype.setCategoryActiveId=function(id){ this.category.id=id; };
當分類目錄下的信息被點選,就從對應的a標記獲取data-id
值。
查找data-id值,不然把data-id設爲null.
寫一個Data對象的addCategory方法。把它添加到點擊事件中。
Data.prototype.addCategory=function(id,category){ var data=this.data; var arr=[]; if(id==null){ arr=data; }else{ (function findPositon(_id,_data){ for(var i=0;i<_data.length;i++){ console.log(_data[i]["id"]) if(_data[i]["id"]==_id){ arr=_data[i]["list"]; } if(_data[i]["list"]!==[]){ findPositon(_id,_data[i]["list"]); } } })(id,this.data); } console.log(arr); arr.push(category); };
而後在監聽事件中,寫一個方法當點擊時把對應a的data-id值存起來:
Data.prototype.setCategoryActiveId=function(id){ this.category.id=id; }; //。。。 //經過事件代理監聽數據 $('.lv1').delegate('a','click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list2').removeClass('classify-active2'); // 頂層加類 $('h3',getAcient(this,'lv1')).addClass('classify-active'); if(this.parentNode.className!=="title-list"){ $(this.parentNode).addClass('classify-active2'); } dataRenderer.setCategoryActiveId(this.getAttribute('data-id')); });
注意,每次渲染後內容都會丟失,
因此添加分類概括起來作這麼幾件事:
$('#newCategory .submit').on('click',function(){ // 獲取激活的a標記的id(在你點選時已經存在了`.category.id`裏) var idName=dataRenderer.category.id; //獲取數據 var value=$('#newCategory .input').obj.value; //構造目錄信息,mission和list固然是空的。 var obj={ "categoryName":value, "id":Date.parse(new Date()), "missions":[], "list":[] }; //添加進去! dataRenderer.addCategory(idName,obj); //根據更新後的數據執行渲染 dataRenderer.renderTree('#categories'); // 添加基本效果。 dataRenderer.renderCategoryEfect(); // 事件監聽,不作這一步的話就再沒法更新信息 $('.lv1').delegate('a','click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list2').removeClass('classify-active2'); // 頂層加類 $('h3',getAcient(this,'lv1')).addClass('classify-active'); if(this.parentNode.className!=="title-list"){ $(this.parentNode).addClass('classify-active2'); } //把a標記的data-id值拿到手 dataRenderer.setCategoryActiveId(this.getAttribute('data-id')); }); // 模態彈窗關閉 document.body.removeChild($('.add').obj); $('.add-mask').obj.style.display='none'; }); });
通過無數次失敗的嘗試和換位思考,目錄樹的結果終於出來了:
原本只想作二級目錄就夠了。如今終於實現多級目錄了
漫長而糾結的分類模塊尚未結束,可是思路已經愈來愈清晰了。接下來要作的是點擊x
,刪除分類。
經過dom查找(這個關閉按鈕的父級的第一個元素),能夠獲得這個分類下的id值。而後寫一個方法,找到該id目錄所在的引用位置,將它用splice抹掉!(不能用filter去重)
方法的核心是一個遞歸,一個循環。
//根據id值刪除分類: Data.prototype.deleteCategory=function(id){ var _this=this; var parentDataArr=[];//描述待刪除數據所在的數組。 var childData={};//描述待刪除對象 (function findPosition(_id,_data){ for(var i=0;i<_data.length;i++){ //console.log(_data[i]["id"]) if(_data[i]["id"]==_id){ parentDataArr=_data; childData=_data[i]; } if(_data[i]["list"]!==[]){ findPosition(_id,_data[i]["list"]); } } })(id,_this.data); for(var i=0;i<parentDataArr.length;i++){ if(parentDataArr[i]==childData){ parentDataArr.splice(i,1); } } };
怎麼調用呢?
主要是渲染後再次綁定——寫一個的函數吧!
function close(){ $('.classify-close2').on('click',function(){ // 獲取id值 var dataId=this.parentNode.childNodes[0].getAttribute('data-id'); // 從數據中刪除該id所在的目錄 dataRenderer.deleteCategory(dataId); // 渲染 dataRenderer.renderTree('#categories'); dataRenderer.renderCategoryEffect(); //再次綁定事件 close(); }); $('.classify-close').on('click',function(){ var dataId=this.parentNode.childNodes[0].getAttribute('data-id'); dataRenderer.deleteCategory(dataId); dataRenderer.renderTree('#categories'); dataRenderer.renderCategoryEffect(); close(); }); $('.lv1').delegate('a','click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list2').removeClass('classify-active2'); $('h3',getAcient(this,'lv1')).addClass('classify-active'); if(this.parentNode.className!=="title-list"){ $(this.parentNode).addClass('classify-active2'); } dataRenderer.setCategoryActiveId(this.getAttribute('data-id')); }); }; close();
這個close函數之因此不作成執行函數,由於在添加時還須要再調用一次。如今close函數已經包含了事件代理,delegate代理在添加目錄後就能夠刪掉了。
var dataRenderer=new Data(json); // 渲染目錄樹 dataRenderer.renderTree('#categories'); dataRenderer.renderCategoryEffect(); // 刪除分類邏輯 function close(){ $('.classify-close2').on('click',function(){ // 獲取id值 var dataId=this.parentNode.childNodes[0].getAttribute('data-id'); // 從數據中刪除該id所在的目錄 dataRenderer.deleteCategory(dataId); // 渲染 dataRenderer.renderTree('#categories'); dataRenderer.renderCategoryEffect(); //再次綁定事件 close(); }); $('.classify-close').on('click',function(){ console.log(1); var dataId=this.parentNode.childNodes[0].getAttribute('data-id'); dataRenderer.deleteCategory(dataId); dataRenderer.renderTree('#categories'); dataRenderer.renderCategoryEffect(); close(); }); // 事件代理 $('.lv1').delegate('a','click',function(){ $('.title-list').removeClass('classify-active'); $('.title-list2').removeClass('classify-active2'); $('h3',getAcient(this,'lv1')).addClass('classify-active'); if(this.parentNode.className!=="title-list"){ $(this.parentNode).addClass('classify-active2'); } dataRenderer.setCategoryActiveId(this.getAttribute('data-id')); }); } close(); // 添加分類邏輯 $('#addCategory').on('click',function(){ var categoryModal=new Modal(); categoryModal.init( { type:'newCategory', title:'目錄' } ); categoryModal.create(); // 添加分類 $('#newCategory .submit').on('click',function(){ var idName=dataRenderer.category.id; var value=$('#newCategory .input').obj.value; var obj={ "categoryName":value, "id":Date.parse(new Date()), "missions":[], "list":[] }; dataRenderer.addCategory(idName,obj); dataRenderer.renderTree('#categories'); dataRenderer.renderCategoryEffect(); // 綁定刪除分類 close(); // 把當前激活的id設置爲null,這是細節處理 dataRenderer.setCategoryActiveId(null); // 模態彈窗關閉 categoryModal.exit(); }); });
效果就出來了,可是仍是有一個細節問題。
經過點擊,就自動獲取了目錄元素的id值,可是當我想建立一級目錄時怎麼辦?
我讓點擊全部分類,就Data對象的id值設爲null。
Data.prototype.clearCategoryId=function(){ $(this.category.id+' *').removeClass('classify-active'); $(this.category.id+' *').removeClass('classify-active2'); this.category.id=null; };
而後在刪除時處理掉。
根據需求,默認分類不可不可刪除(沒有刪除按鈕,天然刪除不了),不能添加子分類(添加分類時出現錯誤提示),但旗下任務能夠添加任務內容。其實就是一個判斷的事情。
實際上這是一個特殊的分類數據結構。就把它的id值設置爲0吧!
好比:
var json = [ { "id":0, "categoryName":"默認分類(不可操做子分類)", "missions":[ { "title":"默認分類示例", "createTime":"1970-1-1", "isCompleted":"true", "content":"完成默認分類說明的撰寫" }, // ... ], "list":[] }, // ...
用前面設計的方法足夠渲染出默認分類了。
首先,在renderTree方法中判斷id值,若是爲‘0’,就不渲染刪除按鈕
Data.prototype.renderTree=function(selector){ var _this=this; var result=''; (function getTree(_data){ var obj=_data; for(var i=0;i<obj.length;i++){ var str=''; if(obj==_this.data){//若是是頂層一級標題則用較大的字體 if(obj[i]["id"]=='0'){//id爲0只可能在設計數據的第一層顯示 str= '<li class="lv1"><h3 class="title-list">'+ '<a '+'data-id="'+obj[i]["id"]+'"'+' class="title1" href="javascript:;"><img src="images/dirs.png"> '+obj[i]["categoryName"]+'</a>'+ '</h3>'; }else{ str= '<li class="lv1"><h3 class="title-list">'+ '<a '+'data-id="'+obj[i]["id"]+'"'+' class="title1" href="javascript:;"><img src="images/dirs.png"> '+obj[i]["categoryName"]+'</a>'+ '<a class="classify-close" href="javascript:;"><img src="images/close.png"></a>'+ '</h3>'; } // 後文略
其次點擊分類添加時,判斷id值是否爲‘0’,是的話就渲染彈出框:
// 添加分類邏輯 $('#addCategory').on('click',function(){ var categoryModal=new Modal(); var idName=dataRenderer.category.id; if(idName=='0'){ categoryModal.init( { type:'tips', tips:'不能爲默認目錄添加子分類!' } ); categoryModal.create(); }else{ categoryModal.init( { type:'newCategory', title:'目錄' } ); categoryModal.create(); } // 後文略
效果:
獲取了Data.category.id
以後,就把數據集的mission獲取到了。
這個方法獨立出來意義不大,只是寫出來測試用:
Data.prototype.findMission=function(id){ var _this=this; var arr=[]; (function findPosition(_id,_data){ //console.log(_data); for(var i=0;i<_data.length;i++){ //console.log(_data[i]["id"]) if(_data[i]["id"]==_id){ arr=_data[i]["missions"]; } if(_data[i]["list"]!==[]){ findPosition(_id,_data[i]["list"]); } } })(id,_this.data); return arr; };
而後在事件代理中加上這麼一句:
console.log(dataRenderer.findMission(this.getAttribute('data-id')));
,每次點擊目錄標題,就在console看到了該分類下的任務內容了!。
中間列爲任務列表,用於顯示當前選中分類下的全部未完成任務。
這在React.js中小菜一疊。可是若是不用框架,會要麻煩些。
正常來講由上至下渲染是最好的。
當沒有頭緒時,把React的思路套進來是不錯的選擇。
傳進來數據,先作一個日期分類的數組。查詢數組中是否存在該日期。沒有則把該對象生成一個ul信息後追加到數組,不然追加到數組的對應的元素中:
Data.prototype.renderMissions=function(selector){ $(selector).obj.innerHTML=''; //獲取原始數組 var categoryId=this.category.id; var _this=this; var data=[]; (function findPosition(_id,_data){ for(var i=0;i<_data.length;i++){ //console.log(_data[i]["id"]) if(_data[i]["id"]==_id){ data=_data[i]["missions"]; } if(_data[i]["list"]!==[]){ findPosition(_id,_data[i]["list"]); } } })(categoryId,_this.data); this.missions.arr=data;//data是存到對象裏方便其它方法調用。 //對數組進行處理 var arr=[]; if(data.length!==0){// 拿到的data數據有多是空數組,空數組之間不相互相等,因此就用長度判斷 for(var i=0;i<data.length;i++){ // 先生成li數據:一個數據名每個關閉按鈕 var li=document.createElement('li'); li.innerHTML='<a href="javascript:;">'+data[i]["title"]+'</a><a class="mission-close" href="javascript:;"><img src="images/close.png" alt="delete"></a>'; // 搜索組件需求 li.setAttribute('data-key', 'true'); li.setAttribute('data-search',"true"); if(data[i]["isCompleted"]){ li.className='completed'; }else{ li.className='uncompleted'; } var bCheck=true; for(var j=0;j<arr.length;j++){ if(arr[j].getAttribute('data-date')==data[i]["createTime"]){ arr[j].appendChild(li); bCheck=false; break; } } // 若是找不到,就要追加新ul if(bCheck){ var ul=document.createElement('ul'); ul.className='todo-date'; ul.innerHTML = '<span>'+data[i]["createTime"]+'</span>'; ul.setAttribute('data-date', data[i]["createTime"]); ul.appendChild(li); arr.push(ul); } } // 最後再經過循環把該ul添加到指定容器 arr.forEach(function(item,index){ $(selector).obj.appendChild(item); }); // 內容渲染完了,須要在這裏綁定效果,好比鼠標懸停效果,刪除邏輯等。 }else{// 若是是空數組就渲染提示信息 $(selector).obj.innerHTML='<p style="margin-top:20px;text-align:center;color:#666;">該分類下尚未任何任務!</p>'; } }; // ... // 在delegate中調用: dataRenderer.renderMissions('#todo-content');
效果:
增長任務基本邏輯是:找到當前任務所屬的分類下的missions
數組(咱們在執行任務渲染時已經把它加到Date.missions.arr裏面了),追加一個任務信息以下:
{ "id":Date.parse(new Date()), "createTime":friendlyDate(), "title":你設定的名字, "isCompleted":false, "content":'' }
其中,日期要轉化爲友好的格式(xxxx-y-z):
function friendlyDate(){ var cDate=new Date().toLocaleDateString().replace(/\//g,'-'); return cDate; }
增長任務須要考慮的問題是:若是我什麼都任務沒點選,目錄信息this.missions.arr
是一個空對象。若是我刪除了一個分類
刪除任務的交互更加複雜一些,首先得有一個相似任務中的關閉按鈕,當鼠標懸停在相應的li標記時,按鈕顯示。當點擊這個按鈕,便可獲取該任務的id值,而後在this.minssions.arr中查找該id所在的任務對象,刪除之,最後渲染之。
在這一步,不須要考慮目錄的問題。
綜上,這兩個方法這樣寫:
Data.prototype.deleteMission=function(id){ var arr=this.missions.arr; for(var i=0;i<arr.length;i++){ if(arr[i]["id"]==id){ arr.splice(i,1); } } this.renderMissions('#todo-content'); }; Data.prototype.addMission=function(option){ var arr=this.missions.arr; arr.push(option); this.renderMissions('#todo-content'); };
那麼怎麼調用呢?和任務樹邏輯相似,甚至還要簡單一點:
$('#addMission').on('click',function(){ var missionCategory=dataRenderer.missions.arr; var missionModal=new Modal(); if(missionCategory===null){ missionModal.init({ type:'tips', tips:'你尚未選擇一個分類!' }); missionModal.create(); }else{ //console.log(missionId); missionModal.init( { type:'newMission', title:'任務' } ); missionModal.create(); $('#newMission .submit').on('click',function(){ var value=$('#newMission .input').obj.value; var option={ "id":Date.parse(new Date()), "createTime":friendlyDate(), "title":value, "isCompleted":false, "content":'' }; dataRenderer.addMission(option); missionModal.exit(); }); } });
如今已經寫了不少個方法。能夠考慮怎麼寫更加方便友好。
初始的任務區應該根據Data.category.id
進行渲染。若是什麼目錄都沒有點選,那麼就不該該顯示目錄相關的內容。
也就是說,每次目錄id值改變,都須要執行Data.renderMissions
方法。
既然那麼麻煩,不如把renderMissions方法寫到內容裏面算了!這在軟件設計中是一個值的考慮的問題。但考慮「高內聚」的原則,這些邏輯仍是得在主要代碼中體現出來,因此不刪除。
好比,我要點擊「全部分類」,要作4件事:
$('#category-all').on('click',function(){ dataRenderer.clearCategoryId(); dataRenderer.missions.arr=null; dataRenderer.renderMissions('#todo-content'); search.clear(); });
數據的流向應該是清理id,觸發當前分類爲null,觸發渲染任務區。
同時,還要把搜索組件裏的key
清理爲'all'
.
第二個,當在搜索欄沒有清空時刪除任務分類,會是什麼狀態?
天然是清理輸入框的數據,把全部
按鈕的激活樣式設置爲激活。
當搜索框還有內容時刪除任務,也要清理輸入框,全部
按鈕的樣式設置爲激活。
第三點,任務樹追加到網頁的DOM結構以後,都要對效果進行綁定。
Data.prototype.renderCategoryEffect=function(){ // 添加懸停效果 $('.mission-close').hover(function(){ $(this).move({ 'opacity':100 }); },function(){ $(this).move({ 'opacity':0 }); }); $('.mission-close').on('click',function(){ var missionId=this.parentNode.childNodes[0].getAttribute('data-missionId'); _this.deleteMission(missionId); $('.todo-btn').removeClass('todo-btn-active'); $('#all').addClass('todo-btn-active'); $('#search').obj.value=''; _this.missions.id=null; }); // 激活樣式 $('#todo-content').delegate('a','click',function(){ if(this.className!=='mission-close'){ $('#todo-content a').removeClass('missions-active'); $(this).addClass('missions-active'); }else{ } }); };
這一段能夠按做爲Data對象渲染任務樹時的內部方法。
綜合以上,就是:
Data.renderMissions
方法。放一個效果:
讓咱們結束繁雜的任務渲染流程,到任務內容的渲染上來吧!
當前的任務的a標記都綁定了一個對應的id值。寫一個getContent方法來獲取整個任務具體對象:
Data.prototype.getContent=function(id){ var arr=this.missions.arr; console.log(arr); for(var i=0;i<arr.length;i++){ if(id==arr[i]["id"]){ return arr[i]; } } };
如今要來獲取這個id任務下的內容。
// 激活樣式 $('#todo-content').delegate('a','click',function(){ if(this.className!=='mission-close'){ $('#todo-content a').removeClass('missions-active'); $(this).addClass('missions-active'); // 如下是內容顯示區 var idName=this.getAttribute('data-missionid'); var content=dataRenderer.getContent(idName); console.log(content.content); } });
那還要不要寫一個渲染方法呢?
答案是不要再折騰了。直接使用marked.js吧!
// 激活樣式 $('#todo-content').delegate('a','click',function(){ if(this.className!=='mission-close'){ $('#todo-content a').removeClass('missions-active'); $(this).addClass('missions-active'); // 如下是內容顯示區 var idName=this.getAttribute('data-missionid'); var content=dataRenderer.getContent(idName); $('.content-substract').obj.innerHTML=content.createTime; $('.content-header h3').obj.innerHTML=content.title; $('.content-paper').obj.innerHTML=marked(content.content); } });
以前作了數據各類展現,但還沒作過數據修改的功能。
修改的邏輯是:點擊編輯按鈕——>編輯按鈕隱藏,提交按鈕出現——>出現任務編輯欄——>在編輯欄輸入數據——>點擊保存——>提交按鈕隱藏,編輯按鈕出現——>查找該任務內容的引用地址,修改該地址下的數據爲文本框輸入的內容。
markdown編輯時要求所見即所得。因此有一個編輯預覽窗口,經過keyup事件傳進去渲染出markdown效果。
$('#edit').on('click',function(){ var idName=dataRenderer.missions.id; var content=dataRenderer.getContent(idName); var str= '標題 <input id="content-title" value='+content.title+' type="text"/><br><p style="line-height:30px; font-size:16px">內容</p><textarea id="content-edit" rows="16" cols="80">'+content.content+'</textarea>'+ '<p style="line-height:30px; font-size:16px">效果預覽:</p><div class="edit-view"></div>'; $('.content-paper').obj.innerHTML=str; this.style.display='none'; $('#content-submit').obj.style.display='block'; // 實時預覽 $('#content-edit').on('keyup',function(){ $('.edit-view').obj.innerHTML = marked(this.value); }); }); $('#content-submit').on('click',function(){ var idName=dataRenderer.missions.id; var content=dataRenderer.getContent(idName); var value=$('#content-edit').obj.value; var title=$('#content-title').obj.value; content.content=value; content.title=title; $('#edit').obj.style.display='block'; this.style.display='none'; $('.content-substract').obj.innerHTML=content.createTime; $('.content-header h3').obj.innerHTML=content.title; $('.content-paper').obj.innerHTML=marked(content.content); $('.missions-active').obj.innerText=title; });
初始建立的任務內容都是標記爲未完成的。如今要完成一個功能就是點擊我已完成
按鈕,該任務變爲已經完成。
$('#hascompleted').on('click',function(){ var idName=dataRenderer.missions.id; var content=dataRenderer.getContent(idName); content.isCompleted=true; $($('.missions-active').obj.parentNode).removeClass('uncompleted'); $($('.missions-active').obj.parentNode).addClass('completed'); });
這個項目一大半的時間其實都在思考數據結構和交互。
只有當點擊任務區時,纔出現任務內容,當任務樹從新渲染,任務內容區的視圖就從新刷新爲歡迎頁面。
當歡迎頁面呈現時,不容許出現編輯按鈕
歡迎頁面其實就是一篇簡單的說明文檔。
再好比說,當渲染任務內容時,我已完成
按鈕要根據isCompleted
進行渲染。
本地儲存依賴localStorage,
localStorage是一個對象,可是它能接受的儲存是字符串,因此json數據必須事先經過json檢測。
在文檔的開頭:
var data=null; if(localStorage.djtaoTodo){ data=eval(localStorage.djtaoTodo); console.log('old'); }else{ console.log('new'); localStorage.djtaoTodo=JSON.stringify(json); data=json; } var dataRenderer=new Data(data); ...
而後在網頁刷新或關閉時,把dataRenderer的data數據存到localStorage的目錄中。
window.onunload=function(){ localStorage.djtaoTodo=JSON.stringify(dataRenderer.data); };
這樣本地儲存的問題就解決了。至此,待辦事項列表的項目算是完成。