我的待办事项工具的设计和搭建(IFE前端2015春季 任务3)

这是我几个月以前的项目做品,花了至关的时间去完善。博客人气不高,但拿代码的人很多,因此一直处于保密状态。没有公开代码。但若是对你有帮助,并能提出指导意见的,我将十分感谢。javascript

IFE前端2015春季 任务3

综合练习

任务描述

参考设计稿实现一个简单的我的任务管理系统:以下图css

设计稿

任务需求描述:html

  • 最左侧为任务分类列表,支持查看全部任务或者查看某个分类下的全部任务
    • 初始时有一个默认分类,进入页面时默认选中默认分类
    • 分类支持多层级别。
    • 分类支持增长分类、删除分类两个操做在左侧分类最下方有添加操做,点击后弹出浮层让输入新分类的名称,新分类将会被添加到当前选中的分类下。浮层能够为自行设计实现,也能够直接使用prompt。当鼠标hover过某一个分类时,右侧会出现删除按钮,点击后,弹出确认是否删除的浮层,确认后删除掉该分类。弹出的确认浮层能够自行设计实现,也能够直接使用confirm。不能为默认分类添加子分类,也不能删除默认分类
    • 每个分类名字后显示一个当前分类下的未完成任务总数量。
  • 中间列为任务列表,用于显示当前选中分类下的全部未完成任务
    • 任务列表按日期(升序或者降序,自行设定)进行聚类
    • 用不一样的字体颜色或者图标来标示任务的状态,任务状态有两张:已完成未完成
    • 下方显示新增任务的按钮,点击后,右侧列会变成新增任务编辑界面。
    • 单击某个任务后,会在右侧显示该任务的详细信息。
    • 在任务列表的上方有任务筛选项,能够选择在任务列表中显示全部任务,或者只显示已完成或者未完成的任务。
  • 右侧为任务详细描述部分
    • 第一行显示任务标题,对于未完成的任务,在标题行的右侧会有完成任务的操做按钮及编辑任务的按钮。
    • 点击完成任务按钮时,弹出确认是否确认完成的浮层,确认后该任务完成,更新中间列任务的状态。弹出的确认浮层能够自行设计实现,也能够直接使用confirm
    • 点击编辑任务操做后,右侧变动为编辑窗口。
  • 新增及编辑任务窗口描述
    • 有3个输入框:分别是标题输入框,完成日期输入框及内容输入框
    • 标题输入框:输入标题,为单行,须要自行设定一个标题输入限制的规则(如字数),并给出良好提示。
    • 日期输入框:单行输入框,按照要求格式输入日期,如yyyy-mm-dd
    • 内容输入框:多行输入框,自行设定一个内容输入的限制(如字数),并给出良好提示。
    • 确认按钮:确认新增或修改。
    • 取消按钮:取消新增或修改。

任务实现要求:前端

  • 整个界面的高度和宽度始终保持和浏览器窗口大小一致。当窗口变化高宽时,界面中的内容自适应变化。
  • 左侧列表和中间列表保持一个固定宽度(自行设定),右侧自适应。
  • 须要自行设定一个最小宽度和最小高度,当浏览器窗口小于最小值时,界面内容的高度和宽度再也不跟随变化,容许浏览器出现滚动条。
  • 经过本地存储来做为任务数据的保存方式。
  • 不使用任何类库及框架。
  • 尽量符合代码规范的要求。
  • 浏览器兼容性要求:Chrome、IE8+。

注意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';
取消按钮(#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-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);
};

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

相关文章
相关标签/搜索