这是我几个月以前的项目做品,花了至关的时间去完善。博客人气不高,但拿代码的人很多,因此一直处于保密状态。没有公开代码。但若是对你有帮助,并能提出指导意见的,我将十分感谢。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\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); };
这样本地储存的问题就解决了。至此,待办事项列表的项目算是完成。