📦 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;全部完成;删除已完成;长按完成;小工具过滤器
浮动球全屏移动;左侧翻转;左滑/右滑todo删除
双击todo进行编辑
长按添加一条todo
横屏适配
核心实现代码
### 语法优化/**
* 通过id快速访问节点
* @param {element id} id
*/
function $(id){
return document.getElementById(id);
}
/**
* 通过类型快速创建节点
* @param {节点类型} type
*/
function $c(type){
return document.createElement(type);
}
/**
* 通过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层
**/
window.model = {
data: {
todos: [
/**
* 【存储实例】
* content: "this is a todo example"
* time:
* completed: false
*/
],
filter: "All",
}
}
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;
}
});
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){
}
}
}
样式结构
<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浏览器顶部的工具条会影响屏幕的
screen.height
,在css中设定100vh
也部位定值,会导致抖动现象,非常影响用户体验。因此该用js绑定innerHeight
和innerWidth
var deviceHeight = window.innerHeight; // 屏幕高度 var deviceWidth = window.innerWidth; // 屏幕宽度 /* 固定屏幕尺寸(手机safari infobar尺寸不固定) */ $('bg').style.height = deviceHeight + "px";
-
safari浏览器不响应
:hover
伪类,因此通过touchstart
和touchend
进行替代 -
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] |
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