這是我幾個月之前的項目作品,花了相當的時間去完善。博客人氣不高,但拿代碼的人不少,所以一直處於保密狀態。沒有公開代碼。但如果對你有幫助,並能提出指導意見的,我將十分感謝。 IFE前端2015春季 任務3 綜合練習 任務描述 參考 "設計稿" 實現一個簡單的個人任務管理系統:如下圖 " " 任務需求描 ...
這是我幾個月之前的項目作品,花了相當的時間去完善。博客人氣不高,但拿代碼的人不少,所以一直處於保密狀態。沒有公開代碼。但如果對你有幫助,並能提出指導意見的,我將十分感謝。
IFE前端2015春季 任務3
綜合練習
任務描述
參考設計稿實現一個簡單的個人任務管理系統:如下圖
任務需求描述:
- 最左側為任務分類列表,支持查看所有任務或者查看某個分類下的所有任務
- 初始時有一個
預設分類
,進入頁面時預設選中預設分類
。 - 分類支持多層級別。
- 分類支持增加分類、刪除分類兩個操作在左側分類最下方有添加操作,點擊後彈出浮層讓輸入新分類的名稱,新分類將會被添加到當前選中的分類下。浮層可以為自行設計實現,也可以直接使用
prompt
。當滑鼠hover
過某一個分類時,右側會出現刪除按鈕,點擊後,彈出確認是否刪除的浮層,確認後刪除掉該分類。彈出的確認浮層可以自行設計實現,也可以直接使用confirm
。不能為預設分類
添加子分類,也不能刪除預設分類
。 - 每一個分類名字後顯示一個當前分類下的未完成任務總數量。
- 中間列為任務列表,用於顯示當前選中分類下的所有未完成任務
- 任務列表按日期(升序或者降序,自行設定)進行聚類
- 用不同的字體顏色或者圖標來標示任務的狀態,任務狀態有兩張:
已完成
或未完成
。 - 下方顯示
新增任務
的按鈕,點擊後,右側列會變成新增任務編輯界面。 - 單擊某個任務後,會在右側顯示該任務的詳細信息。
- 在任務列表的上方有任務篩選項,可以選擇在任務列表中顯示所有任務,或者只顯示
已完成
或者未完成
的任務。 - 右側為任務詳細描述部分
- 第一行顯示任務標題,對於未完成的任務,在標題行的右側會有
完成任務
的操作按鈕及編輯任務
的按鈕。 - 點擊
完成任務
按鈕時,彈出確認是否確認完成的浮層,確認後該任務完成,更新中間列任務的狀態。彈出的確認浮層可以自行設計實現,也可以直接使用confirm
。 - 點擊
編輯任務
操作後,右側變更為編輯視窗。 - 新增及編輯任務視窗描述
- 有3個輸入框:分別是標題輸入框,完成日期輸入框及內容輸入框
- 標題輸入框:輸入標題,為單行,需要自行設定一個標題輸入限制的規則(如字數),並給出良好提示。
- 日期輸入框:單行輸入框,按照要求格式輸入日期,如yyyy-mm-dd
- 內容輸入框:多行輸入框,自行設定一個內容輸入的限制(如字數),並給出良好提示。
- 確認按鈕:確認新增或修改。
- 取消按鈕:取消新增或修改。
任務實現要求:
- 整個界面的高度和寬度始終保持和瀏覽器視窗大小一致。當視窗變化高寬時,界面中的內容自適應變化。
- 左側列表和中間列表保持一個固定寬度(自行設定),右側自適應。
- 需要自行設定一個最小寬度和最小高度,當瀏覽器視窗小於最小值時,界面內容的高度和寬度不再跟隨變化,允許瀏覽器出現滾動條。
- 通過本地存儲來作為任務數據的保存方式。
- 不使用任何類庫及框架。
- 儘可能符合代碼規範的要求。
- 瀏覽器相容性要求:Chrome、IE8+。
註意
該設計稿僅為線框原型示意圖,所有的視覺設計不需要嚴格按照示意圖。如果有設計能力的同學,歡迎實現得更加美觀,如果沒有,也可以按照線框圖實現。以下內容可以自行發揮:
- 背景顏色
- 字體大小、顏色、行高
- 線框粗細、顏色
- 圖標、圖片
- 高寬、內外邊距
解決方案
整個環境應該通過後端的交互實現。但是簡單地實現就是ajax方法。
項目要求不用任何類庫框架,但是任務2中的$d
類庫是自己寫的。可以檢驗$d
類庫的可靠性,所以用了也問題不大。
待辦事項列表是一個相當典型的數據結構,再設計數據結構時,顯然應該用面向對象的思路觸發操作。
基本樣式和交互
第一個問題就是高度自填充。
和寬度一樣,一個元素要在父級高度有數值時才能設定百分比高度。
分類列表
分類列表的方法應該是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';
取消按鈕(#exit)
本著清理乾凈的精神,除了把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';
}
實際上
到此可以認為,這個靜態的模態彈窗完成。
效果:
markdown組件
雖然任務要求不用任何框架,但是我們的需求在當前來說已經開始超越了任務本身的需求,不用jQuery勉強可以接受,但是前端渲染你的content部分內容,marke.js顯然是最好的選擇。關於marked.js的用法,可以參照marked.js簡易手冊。
實際上這已經是第三次在項目中用到mark.js,用起來水到渠成。
當然不想做任何處理的話,也可以跳過這節。
引入marked.js和highlight.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;
}
- 列表效果(其實也包括ol-li)
.content-paper ul li{
list-style: disc;
margin-left: 15px;
}
.content-paper ol li{
list-style: decimal;
margin-left: 15px;
}
- 文本間距,行間距,比如,p標記,h1-h6的間距等等。大小最好用em和百分比顯示,比如我的p標記字體大小為
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<