• 个人待办事项工具的设计和搭建(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{
    	 auto;
    	height: inherit;
    	padding-left: 512px;
    }
    .content-outer{
    	height: 100%;
    	position: relative;
    }
    .content-info{
    	height: 91px;
    }
    .content-content{
    	position: absolute;
    	 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模块开发
    ---
    Rendered by **marked**.
    
    ```javascript
    function(){
      console.log("Hello!Marked.js!");
    }
    ```
    这是响应式图片测试:
    ![](http://images2015.cnblogs.com/blog/1011161/201701/1011161-20170127184909206-861797658.png)
    1. 传进去前端的代码结构必须符合样式库的要求。
    2. 我要把页面的代码统统是现货高亮显示——比如这样`alert(Hello!)`');
    
    重写样式库

    尽管有了样式库的支持,但是这个样式库只是定义了配色。而浏览器默认的样式被当初的css-reset给干掉了。

    markdown最常用的效果就是代码高亮,搭配图片显示,

    在过去的项目(Node.js博客搭建)中,我已经使用了marked.js重写了一个还算漂亮的样式库(基于marked.js样式库和bootstrap样式库code和pre部分)。现在把重写CSS的要点简单归纳如左:

    • 响应式图片
    .content-paper img{
      display: block;
      max- 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('
    '));
    };
    
    $(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-searchdata-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');
    	});
    

    最终效果:

    数据可视化

    数据可视化是个大坑。

    基本逻辑是:

    • 分类模块从后端获取数据根据数据进行分类展示
      • 当分类被点选,则暴露该分类下的一级任务信息给任务模块,
      • 点击创建模块,根据当前层级,创建一个平级的分类,如果没有点选,则创建一个一级分类。
    • 任务模块根据暴露出来的信息,按照“创建日期”的逻辑重新分类并进行排列
      • 筛选组件查找暴露出来的信息,按照筛选规则重新排列
      • 根据分类区块的点选结果,暴露一个当前选择的任务给右侧的信息内容模块
    • 右侧信息内容模块根据任务模块暴露出来的信息,用markdown渲染内容并进行显示。

    显然用面向对象的思路是最好的。

    原始数据结构的设计

    涉及无级树的设计。

    纵观前面的逻辑,每个数据需要哪些特性?

    一个好的数据结构,前端拿到之后渲染也是方便的。不妨直观一点,用数组+对象的方式来组织信息。

    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对象暴露任务内容给中间的任务栏

    获取了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对象渲染任务树时的内部方法。

    综合以上,就是:

    • 每次在渲染任务树时,都把搜索组件的内容初始化。
    • 每次目录id值改变,都需要执行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);
    };
    

    这样本地储存的问题就解决了。至此,待办事项列表的项目算是完成。

  • 相关阅读:
    SpringBoot-10-之初阶整合篇(下)
    09--SpringBoot之初阶整合篇(上)
    07--SpringBoot之数据库JPA(CRUD)
    go 文件操作 io
    类型断言
    多态
    golang interface
    go strcut 封装
    go struct 抽象
    poj-3280 Cheapest Palindrome (dp)
  • 原文地址:https://www.cnblogs.com/djtao/p/6826985.html
Copyright © 2020-2023  润新知