Skip to content

📋TodoMVC based on native HTML+JS+CSS | Tongji Univ. SSE Web Programming Course Project

Notifications You must be signed in to change notification settings

doubleZ0108/TodoMVC

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 

Repository files navigation

TodoMVC

🌐Zhe ZHANG's TodoMVC

📦 github仓库



背景介绍

TodoMVC 是一个开源项目,实现了一个 Todo Application,广泛用于 MV* 框架的选择

  • 功能,对应数据库表中记录的增删查改操作(CRUD),TodoMVC 功能是完备的,同时还有表单编辑(edit)功能,过滤(filter)功能等。
  • 扩展,TodoMVC 本身没有实现网络,本地数据存储,路由等功能,教学过程中可以很方便地扩展这些功能,让学生对所学功能有一个更全面的认识;这个过程很有趣,因为你在通过所学知识逐渐让 TodoMVC 变得更加实用。
  • 实用,大量框架和语言实现了 TodoMVC,这带来了一个额外的优势: 一旦熟悉了 TodoMVC,今后熟悉一门新框架(或语言、工具)的成本就更低了,而且不同语言的异同点也会更加直观。

功能介绍

基础功能

  • 新增todo
  • 删除todo
  • 展现todo列表
  • 全部完成/未完成
  • 删除已完成
  • 保存页面状态,刷新页面后可恢复

高级功能

  • 过滤 (All / Active / Completed)
  • 双击编辑单条todo

创意交互

  • 便利贴特效
    • 长按删除
    • 左滑 / 右滑删除
  • 浮动小工具球
    • 拖动自定义位置
    • 左右翻转
    • 单击展开小工具
    • 长按弹出添加todo输入框
  • 聚光灯特效
  • 波浪特效

其他

  • 适配系统主题 (浅色 / 深色)
  • 适配手机横竖屏
  • 字体大小自适应
  • ios Safari浏览器优化(只通过iPhone11进行测试)
    • 顶部工具条尺寸优化
    • hover事件优化
    • 删除todo手机震动提示(trick可遇不可求)

功能展示

便利贴展示todo;全部完成;删除已完成;长按完成;小工具过滤器

IMG_3675IMG_3676

浮动球全屏移动;左侧翻转;左滑/右滑todo删除

IMG_3683IMG_3680

双击todo进行编辑

IMG_3682IMG_3681

长按添加一条todo

IMG_3679IMG_3677

横屏适配

image-20200626222240953


核心实现

核心实现代码 ### 语法优化

id选择器

/**
 * 通过id快速访问节点
 * @param {element id} id 
 */
function $(id){
    return document.getElementById(id);
}

创建DOM节点

/**
 * 通过类型快速创建节点
 * @param {节点类型} type 
 */
function $c(type){
    return document.createElement(type);
}

css选择器

/**
 * 通过css选择器快速访问一组对象
 *    注意返回值为NodeList
 * @param {css选择器} css_selector 
 */
function $all(css_selector){
    return document.querySelectorAll(css_selector);
}

设置样式

/**
 * 快速设置节点样式
 * @param {节点对象} obj 
 * @param {一组css对象} css 
 */
function setStyle(obj, css){
    for(let atr in css){
        obj.style[atr] = css[atr];
    }
}

数据持久化存储

(function(){
    if(!window.localStorage){
        alert("您的浏览器不支持Local Storage");
        return false;
    } else {
        let key = "todos";
        Object.assign(model, {
            /**
             * 读取LocalStorage进行初始化
             **/
            init: function(callback){
                let data = window.localStorage.getItem(key);
                if(data){ 
                    model.data = JSON.parse(data);
                }
                if(callback) { callback(); }
            },
            /**
             * 写入LocalStorage进行持久化
             **/
            flush: function(callback){
                window.localStorage.setItem(key, JSON.stringify(model.data));
                if(callback) { callback(); }
            }
        });
    }
})();

Model

/**
 * Model层
 **/
window.model = {
    data: {
        todos: [
            /**
             * 【存储实例】
             * content: "this is a todo example"
             * time: 
             * completed: false
             */
        ],
        filter: "All",
    }
}

事件监听

手机端模拟double touch

var click_counter = 0;
elem.addEventListener("touchstart", function () {
    touchStartTimer = new Date();
    click_counter++;
    setTimeout(function () {
        click_counter = 0;
    }, dbltouch_interval);
    if (click_counter > 1) {
        console.log("simulate double touch on mobile...");

        click_counter = 0;
    }
});

手机端模拟long touch

let touchStartTimer, touchEndTimer;
btnGroupTouchHandler = {
    start: function(event){
        touchStartTimer = new Date();
    },
    end: function(event){
        touchEndTimer = new Date();
        let deltaTime = touchEndTimer.getTime() - touchStartTimer.getTime();

        /* 长按判定 */
        if(deltaTime > longtouch_interval){
           
        }
    }
}

便利贴Todo

样式结构

<div class="todo-group" id="todo-1">
    <div class="todo-shadow"></div>
    <div class="todo-paper" style="transform: rotate(1.3deg);">
        <div class="todo-paper-bg" id="todo-bgcolor-1">
        </div>
    </div>
    <div class="cover-content-container">
        <div class="cover-content">
            <p id="todo-text-0" class="todo-text" style="transform: rotate(1.3deg);">这里是一条测试todo</p>
            <input class="editing" type="text" autofocus style="transform: rotate(1.3deg);" />
        </div>
    </div>
</div>

touchstart

let oldTouch, touchObj;
let isDelete = false;
elem.addEventListener('touchstart', function (event) {
    oldTouch = event.touches[0];
    touchObj = event.currentTarget;
    isDelete = false;
}, false);

touchmove

elem.addEventListener('touchmove', function (event) {
    let freshTouch = event.touches[0];
    let verticalOffset = freshTouch.clientY - oldTouch.clientY;

    if (Math.abs(verticalOffset) < tolerateVerticalOffset) {    // 上下滑动容忍之内视作成功
        var horizontalOffset = freshTouch.clientX - oldTouch.clientX;
        touchObj.style.transition = ".2s linear";

        if (Math.abs(horizontalOffset) < deviceWidth / 3) {     //移动距离过短 不判定为删除
            touchObj.style.left = horizontalOffset + 'px';
        } else {
            if (horizontalOffset < 0) {     // 左滑
                touchObj.style.left = -deviceWidth * 2 + 'px';
            } else {                        // 右滑
                touchObj.style.left = deviceWidth * 2 + 'px';
            }
            isDelete = true;
        }
    }
}, false);

touchend

elem.addEventListener('touchend', function (event) {
    /* 在DOM中和Model中删除该todo */
    if (isDelete && elem != null) {
        elem.parentNode.removeChild(elem);
        model.data.todos.splice(index, 1);

        model.flush();
        update();
    } else {
        touchObj.style.left = 0;
    }
}, false);

浮动球

跟随手指移动

let oldTouch;
btnGroupTouchHandler = {
    start: function(event){
        oldTouch = event.touches[0];
    },
    move: function(event){
        let freshTouch = event.touches[0];
    
        let deltaRight = oldTouch.clientX - freshTouch.clientX;
        let deltaBottom = oldTouch.clientY - freshTouch.clientY;
        let right = parseFloat(btnGroup.style.right || 0) + deltaRight;
        let bottom = parseFloat(btnGroup.style.bottom || 0) + deltaBottom;
    
        /* 跟随手指移动浮动球 */
        if(right < deviceWidth - 60 && right > 0        // 边界检测
            && bottom < deviceHeight - 300 && bottom > 0){
            setStyle(btnGroup, {
                right: right + "px",
                bottom: bottom + "px"
            });
        }
        
        oldTouch = freshTouch;
    }
}

适配屏幕左/右侧

 /* 浮动球移动到左边进行反转 */
 if(right > (deviceWidth - 60) / 2){
    setStyle(btnGroup, {
        transform: "translateY(-30px) rotateY(180deg)"      // 先将整体翻转180
    });
    Array.from($all('.ButtonGroup a i')).forEach(function(elem){
        elem.style.transform = "rotateY(180deg)";           // 再将每个元素翻转180
    });
} else {
    btnGroup.style.transform = "translateY(-30px)";
    Array.from($all('.ButtonGroup a i')).forEach(function(elem){
        elem.style.transform = "none";
    });
}

除此之外

系统主题自适应

通过native CSS实现

@media (prefers-color-scheme: dark) {
    .HeaderSubGroup span {
        color: rgba(255, 255, 255, 0.6);
    }
}

@media (prefers-color-scheme: light) {
    .HeaderSubGroup span {
        color: rgba(0, 0, 0, 0.6);
    }
}

字体大小自适应

初始时使用vh和vw进行字体大小设定,“viewpoint” = window size

  • 15vw = 15% 设置width(可以理解为宽度单位)
  • 15vh = 15% 设置height(可以理解高度单位)

但由于兼容性不好,该用rem进行字体自适应,设定html根节点25px,其余字体大小通过1.2rem进行调整,只需要再通过@media进行根节点调整即可

safari浏览器优化

  1. safari浏览器顶部的工具条会影响屏幕的screen.height,在css中设定100vh也部位定值,会导致抖动现象,非常影响用户体验。因此该用js绑定innerHeightinnerWidth

    var deviceHeight = window.innerHeight;      // 屏幕高度
    var deviceWidth = window.innerWidth;        // 屏幕宽度
    
    /* 固定屏幕尺寸(手机safari infobar尺寸不固定) */
    $('bg').style.height = deviceHeight + "px";
  2. safari浏览器不响应:hover伪类,因此通过touchstarttouchend进行替代

  3. safari浏览器不支持rotateY,拥有该属性的dom节点会直接不显示。解决方法是在父节点上增加perspective属性,并确定位置

    .float-btns {
        transform: perspective(400);
        position: fixed;
        bottom: 0;
        right: 0;
    }

开发环境

  • 操作系统
    • 开发环境:macOS Catalina 10.15.4
    • 部署环境:Ubuntu 16.04.6 LTS
  • 测试环境:
    • Safari on iPhone11
    • Chrome Device Simulator
  • IDE:Visual Studio Code 1.45.1
  • 开发语言
    • HTML5
    • CSS3
    • JavaScript

关于作者

Item VALUE
Name 张喆
ID 1754060
Adviser 徐凯老师(阿里巴巴) 梁爽老师
Course Name Web系统与技术
Course Time 星期五 2-4 [1-8]
星期六 3-6 [11-17]
Email dbzdbz@tongji.edu.cn

项目结构

.
├── README.md
├── TodoMVC.html
└── static
    ├── css
    │   ├── TodoMVC.css
    │   ├── button.css
    │   ├── footer.css
    │   ├── header.css
    │   ├── popup.css
    │   └── todo.css
    ├── img
    │   ├── dark-bg.jpg
    │   └── light-bg.jpg
    └── js
        ├── TodoMVC.js
        ├── button.js
        ├── model.js
        ├── popup.js
        ├── storage.js
        ├── todo.js
        └── util.js

4 directories, 17 files