操作前后效果:(知识点和完整代码在最后面)
bilibili在线视频演示链接:
完整代码:
<!DOCTYPE html>
<!-- Coding By CodingNepal - youtube.com/codingnepal -->
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>【前端实例代码】使用在 HTML CSS 和 JavaScript 创建todolist待办事项列表应用</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- Iconscout Link For Icons -->
<link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.0/css/line.css">
<style>
/* Import Google Font - Poppins */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
width: 100%;
height: 100vh;
overflow: hidden;
background: linear-gradient(135deg, #4AB1FF, #2D5CFE);
}
::selection {
color: #fff;
background: #3C87FF;
}
.wrapper {
max-width: 405px;
padding: 28px 0 30px;
margin: 137px auto;
background: #fff;
border-radius: 7px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.task-input {
height: 52px;
padding: 0 25px;
position: relative;
}
.task-input img {
top: 50%;
position: absolute;
transform: translate(17px, -50%);
}
.task-input .el-icon-notebook-2 {
top: 50%;
position: absolute;
transform: translate(17px, -50%);
}
.task-input input {
height: 100%;
width: 100%;
outline: none;
font-size: 18px;
border-radius: 5px;
padding: 0 20px 0 53px;
border: 1px solid #999;
}
.task-input input:focus,
.task-input input.active {
padding-left: 52px;
border: 2px solid #3C87FF;
}
.task-input input::placeholder {
color: #bfbfbf;
}
.controls, li {
display: flex;
align-items: center;
justify-content: space-between;
}
.controls {
padding: 18px 25px;
border-bottom: 1px solid #ccc;
}
.filters span {
margin: 0 8px;
font-size: 17px;
color: #444444;
cursor: pointer;
}
.filters span:first-child {
margin-left: 0;
}
.filters span.active {
color: #3C87FF;
}
.controls .clear-btn {
border: none;
opacity: 0.6;
outline: none;
color: #fff;
cursor: pointer;
font-size: 13px;
padding: 7px 13px;
border-radius: 4px;
letter-spacing: 0.3px;
pointer-events: none;
transition: transform 0.25s ease;
background: linear-gradient(135deg, #1798fb 0%, #2D5CFE 100%);
}
.clear-btn.active {
opacity: 0.9;
pointer-events: auto;
}
.clear-btn:active {
transform: scale(0.93);
}
.task-box {
margin-top: 20px;
margin-right: 5px;
padding: 0 20px 10px 25px;
}
.task-box.overflow {
overflow-y: auto;
max-height: 300px;
}
.task-box::-webkit-scrollbar {
width: 5px;
}
.task-box::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 25px;
}
.task-box::-webkit-scrollbar-thumb {
background: #e6e6e6;
border-radius: 25px;
}
.task-box .task {
list-style: none;
font-size: 17px;
margin-bottom: 18px;
padding-bottom: 16px;
align-items: flex-start;
border-bottom: 1px solid #ccc;
}
.task-box .task:last-child {
margin-bottom: 0;
border-bottom: 0;
padding-bottom: 0;
}
.task-box .task label {
display: flex;
align-items: flex-start;
}
.task label input {
margin-top: 7px;
accent-color: #3C87FF;
}
.task label p {
user-select: none;
margin-left: 12px;
word-wrap: break-word;
}
.task label p.checked {
text-decoration: line-through;
}
.task-box .settings {
position: relative;
}
.settings :where(i, li) {
cursor: pointer;
}
.settings .task-menu {
z-index: 10;
right: -5px;
bottom: -65px;
padding: 5px 0;
background: #fff;
position: absolute;
border-radius: 4px;
transform: scale(0);
transform-origin: top right;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.15);
transition: transform 0.2s ease;
}
.task-box .task:last-child .task-menu {
bottom: 0;
transform-origin: bottom right;
}
.task-box .task:first-child .task-menu {
bottom: -65px;
transform-origin: top right;
}
.task-menu.show {
transform: scale(1);
}
.task-menu li {
height: 25px;
font-size: 16px;
margin-bottom: 2px;
padding: 17px 15px;
cursor: pointer;
justify-content: flex-start;
}
.task-menu li:last-child {
margin-bottom: 0;
}
.settings li:hover {
background: #f5f5f5;
}
.settings li i {
padding-right: 8px;
}
@media (max-width: 400px) {
body {
padding: 0 10px;
}
.wrapper {
padding: 20px 0;
}
.filters span {
margin: 0 5px;
}
.task-input {
padding: 0 20px;
}
.controls {
padding: 18px 20px;
}
.task-box {
margin-top: 20px;
margin-right: 5px;
padding: 0 15px 10px 20px;
}
.task label input {
margin-top: 4px;
}
}
</style>
</head>
<body>
<div class="wrapper">
<div class="task-input">
<!-- <img src="bars-icon.svg" alt="icon">-->
<i class="el-icon-notebook-2"></i>
<input type="text" placeholder="Add a new task">
</div>
<div class="controls">
<div class="filters">
<span class="active" id="all">All</span>
<span id="pending">Pending</span>
<span id="completed">Completed</span>
</div>
<button class="clear-btn">Clear All</button>
</div>
<ul class="task-box"></ul>
</div>
<script>
const taskInput = document.querySelector(".task-input input"),
filters = document.querySelectorAll(".filters span"),
clearAll = document.querySelector(".clear-btn"),
taskBox = document.querySelector(".task-box");
let editId,
isEditTask = false,
todos = JSON.parse(localStorage.getItem("todo-list"));
filters.forEach(btn => {
btn.addEventListener("click", () => {
document.querySelector("span.active").classList.remove("active");
btn.classList.add("active");
showTodo(btn.id);
});
});
function showTodo(filter) {
let liTag = "";
if (todos) {
todos.forEach((todo, id) => {
let completed = todo.status == "completed" ? "checked" : "";
if (filter == todo.status || filter == "all") {
liTag += `<li class="task">
<label for="${id}">
<input onclick="updateStatus(this)" type="checkbox" id="${id}" ${completed}>
<p class="${completed}">${todo.name}</p>
</label>
<div class="settings">
<i onclick="showMenu(this)" class="uil uil-ellipsis-h"></i>
<ul class="task-menu">
<li onclick='editTask(${id}, "${todo.name}")'><i class="uil uil-pen"></i>Edit</li>
<li onclick='deleteTask(${id}, "${filter}")'><i class="uil uil-trash"></i>Delete</li>
</ul>
</div>
</li>`;
}
});
}
taskBox.innerHTML = liTag || `<span>You don't have any task here</span>`;
let checkTask = taskBox.querySelectorAll(".task");
!checkTask.length ? clearAll.classList.remove("active") : clearAll.classList.add("active");
taskBox.offsetHeight >= 300 ? taskBox.classList.add("overflow") : taskBox.classList.remove("overflow");
}
showTodo("all");
function showMenu(selectedTask) {
let menuDiv = selectedTask.parentElement.lastElementChild;
menuDiv.classList.add("show");
document.addEventListener("click", e => {
if (e.target.tagName != "I" || e.target != selectedTask) {
menuDiv.classList.remove("show");
}
});
}
function updateStatus(selectedTask) {
let taskName = selectedTask.parentElement.lastElementChild;
if (selectedTask.checked) {
taskName.classList.add("checked");
todos[selectedTask.id].status = "completed";
} else {
taskName.classList.remove("checked");
todos[selectedTask.id].status = "pending";
}
localStorage.setItem("todo-list", JSON.stringify(todos))
}
function editTask(taskId, textName) {
editId = taskId;
isEditTask = true;
taskInput.value = textName;
taskInput.focus();
taskInput.classList.add("active");
}
function deleteTask(deleteId, filter) {
isEditTask = false;
todos.splice(deleteId, 1);
localStorage.setItem("todo-list", JSON.stringify(todos));
showTodo(filter);
}
clearAll.addEventListener("click", () => {
isEditTask = false;
todos.splice(0, todos.length);
localStorage.setItem("todo-list", JSON.stringify(todos));
showTodo()
});
taskInput.addEventListener("keyup", e => {
let userTask = taskInput.value.trim();
if (e.key == "Enter" && userTask) {
if (!isEditTask) {
todos = !todos ? [] : todos;
let taskInfo = {name: userTask, status: "pending"};
todos.push(taskInfo);
} else {
isEditTask = false;
todos[editId].name = userTask;
}
taskInput.value = "";
localStorage.setItem("todo-list", JSON.stringify(todos));
showTodo(document.querySelector("span.active").id);
}
});
</script>
</body>
</html>
涉及到的知识点:
1、HTML DOM querySelector() 方法
querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素。
注意: querySelector() 方法仅仅返回匹配指定选择器的第一个元素。如果你需要返回所有的元素,请使用 querySelectorAll() 方法替代。
语法
document.querySelector(CSS selectors)
参数值
参数 | 类型 | 描述 |
---|---|---|
CSS 选择器 | String | 必须。指定一个或多个匹配元素的 CSS 选择器。 可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。 对于多个选择器,使用逗号隔开,返回一个匹配的元素。 提示: 更多 CSS 选择器,请参阅我们的 CSS 选择器参考手册。 |
技术细节
DOM 版本: | Selectors Level 1 Document Object |
---|---|
返回值: | 匹配指定 CSS 选择器的第一个元素。 如果没有找到,返回 null。如果指定了非法选择器则 抛出 SYNTAX_ERR 异常。 |
推荐阅读:
2、HTML DOM addEventListener() 方法
定义和用法
addEventListener() 方法用于向指定元素添加事件句柄。
提示: 使用 removeEventListener() 方法来移除 addEventListener() 方法添加的事件句柄。
语法
element.addEventListener(event, function, useCapture)
参数值
参数 | 描述 |
---|---|
event | 必须。字符串,指定事件名。 注意: 不要使用 "on" 前缀。 例如,使用 "click" ,而不是使用 "onclick"。 提示: 所有 HTML DOM 事件,可以查看我们完整的 HTML DOM Event 对象参考手册。 |
function | 必须。指定要事件触发时执行的函数。 当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, "click" 事件属于 MouseEvent(鼠标事件) 对象。 |
useCapture | 可选。布尔值,指定事件是否在捕获或冒泡阶段执行。 可能值:
|
推荐阅读:
3、HTML DOM classList 属性
定义和用法
classList 属性返回元素的类名,作为 DOMTokenList 对象。
该属性用于在元素中添加,移除及切换 CSS 类。
classList 属性是只读的,但你可以使用 add() 和 remove() 方法修改它。
语法
element.classList
Properties
属性 | Description |
---|---|
length | 返回类列表中类的数量 该属性是只读的 |
推荐阅读:
4、HTML DOM innerHTML 属性
定义和用法
innerHTML 属性设置或返回表格行的开始和结束标签之间的 HTML。
语法
HTMLElementObject.innerHTML=text
4、forEach() 方法
5、map() 方法
6、filter()方法
7、join()方法
8、toLowerCase()方法
9、es6模板字符串