todo 案例

  • 原html

html
css

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Document</title>

  <link rel="stylesheet" href="index.css">
</head>

<body>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <div class="todo-header">
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
        <ul class="todo-main">
          <li>
            <label>
              <input type="checkbox" />
              <span>xxxxx</span>
            </label>
            <button class="btn btn-danger" style="display:none">删除</button>
          </li>
          <li>
            <label>
              <input type="checkbox" />
              <span>yyyy</span>
            </label>
            <button class="btn btn-danger" style="display:none">删除</button>
          </li>
        </ul>
        <div class="todo-footer">
          <label>
            <input type="checkbox" />
          </label>
          <span>
            <span>已完成0</span> / 全部2
          </span>
          <button class="btn btn-danger">清除已完成任务</button>
        </div>
      </div>
    </div>
  </div>
</body>

</html>

/*base*/
body {
  background: #fff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}

基础示例

html
css
js

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Document</title>

  <link rel="stylesheet" href="index.css">
</head>

<body>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <div class="todo-header">
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
        <ul class="todo-main">
        </ul>
        <div class="todo-footer">
          <label>
            <input type="checkbox" />
          </label>
          <span>
            <span>已完成<span id="finish_num">0</span></span></span> / 全部<span id="all_num">0</span>
          </span>
          <button class="btn btn-danger">清除已完成任务</button>
        </div>
      </div>
    </div>
  </div>
  <script src="./index.js"></script>
</body>
</html>

/*base*/
body {
  background: #fff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}

const input = document.querySelector('.todo-header input')
const list = document.querySelector('.todo-main')
const finishNumSpan = document.querySelector('#finish_num')
const allNumSpan = document.querySelector('#all_num')

const todos = [
  { id: "001", name: "抽烟", done: true },
  { id: "002", name: "喝酒", done: true },
  { id: "003", name: "开车", done: false },
]

// 返回todo 已完成,和总共的数量
function updateTodoInfo() {
  let finishNum = 0
  todos.forEach(todo => {
    if (todo.done) {
      finishNum++
    }
  })

  finishNumSpan.innerHTML = finishNum
  allNumSpan.innerHTML = todos.length
}

window.onload = function () {
  todos.forEach(todo => addTodo(todo))
}

// 添加任务 
/*
  @param {Object} todo: {
    id: string;
    name: string;
    done: boolean;
}
*/
function addTodo(todo) {
  const li = document.createElement('li')
  const checked = todo.done ? 'checked' : ''
  const display = todo.done ? 'inline-block' : 'none'
  li.id = todo.id

  li.innerHTML = `
    <label>
      <input id="change-${todo.id}" onchange="handleTodoChange(this)" type="checkbox" ${checked}/>
      <span>${todo.name}</span>
    </label>
    <button id="remove-${todo.id}" onclick="handleTodoDelete(this)" class="btn btn-danger" style="display:${display}">删除</button>
  `

  list.appendChild(li)
  updateTodoInfo()

}

function removeTodo(id) {
  const li = document.getElementById(id)
  console.log('li:', li)
  li.parentNode.removeChild(li)

  const index = todos.findIndex(todo => todo.id === id);

  if (index !== -1) {
    // 使用 splice() 方法删除找到的对象
    todos.splice(index, 1);
  }

  updateTodoInfo()
}

function changeTodo(id, done) {
  const removeID = "remove-" + id
  const removeBotton = document.getElementById(removeID)
  if (done) {
    removeBotton.display = "inline-block"
  } else {
    removeBotton.display = "none"
  }
  todos.forEach(todo => {
    if (todo.id === id) {
      todo.done = done
    }
  })
  updateTodoInfo()
}

// 按钮添加任务
input.addEventListener('keyup', function (e) {
  if (e.key === 'Enter') {
    const value = this.value.trim()
    if (!value) return

    todo = {
      id: `${Date.now()}`,
      name: value,
      done: false
    }

    addTodo(todo)

    this.value = ''
  }
})


function handleTodoChange(target) {
  console.log('target:', target)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  const checked = target.checked
  changeTodo(todoID, checked)
}

function handleTodoDelete(target) {
  console.log('target:', target)
  console.log('target.parentNode:', target.parentNode)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  removeTodo(todoID)
}

完整示例

html
css
js

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Document</title>

  <link rel="stylesheet" href="index.css">
</head>

<body>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <div class="todo-header">
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
        <ul class="todo-main">
        </ul>
        <div class="todo-footer">
          <label>
            <input type="checkbox" />
          </label>
          <span>
            <span>已完成<span id="finish_num">0</span></span></span> / 全部<span id="all_num">0</span>
          </span>
          <button class="btn btn-danger">清除已完成任务</button>
        </div>
      </div>
    </div>
  </div>
  <script src="./index.js"></script>
</body>
</html>

/*base*/
body {
  background: #fff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}

const input = document.querySelector('.todo-header input')
const list = document.querySelector('.todo-main')
const allCheckBox = document.querySelector('.todo-footer input')
const finishNumSpan = document.querySelector('#finish_num')
const allNumSpan = document.querySelector('#all_num')

const todos = [
  { id: "001", name: "抽烟", done: true },
  { id: "002", name: "喝酒", done: true },
  { id: "003", name: "开车", done: false },
]

// 返回todo 已完成,和总共的数量
function updateTodoInfo() {
  let finishNum = 0
  console.log('todos:', todos)
  todos.forEach(todo => {
    if (todo.done) {
      finishNum++
    }
  })
  console.log('finishNum:', finishNum)
  finishNumSpan.innerHTML = finishNum
  allNumSpan.innerHTML = todos.length

  allCheckBox.checked = finishNum === todos.length
}

window.onload = function () {
  todos.forEach(todo => addTodo(todo))
}

// 添加任务 
/*
  @param {Object} todo: {
    id: string;
    name: string;
    done: boolean;
}
*/
function addTodo(todo) {
  const li = document.createElement('li')
  const checked = todo.done ? 'checked' : ''
  const display = todo.done ? 'inline-block' : 'none'
  li.id = todo.id

  li.innerHTML = `
    <label>
      <input id="change-${todo.id}" onchange="handleTodoChange(this)" type="checkbox" ${checked}/>
      <span>${todo.name}</span>
    </label>
    <button id="remove-${todo.id}" onclick="handleTodoDelete(this)" class="btn btn-danger" style="display:${display}">删除</button>
  `

  list.appendChild(li)

  updateTodoInfo()

}

function removeTodo(id) {
  const li = document.getElementById(id)
  console.log('li:', li)
  li.parentNode.removeChild(li)

  const index = todos.findIndex(todo => todo.id === id);

  if (index !== -1) {
    // 使用 splice() 方法删除找到的对象
    todos.splice(index, 1);
  }

  updateTodoInfo()
}

function changeTodo(id, done) {
  console.log(`in changeTodo id:${id}, done:${done}`)
  const removeID = "remove-" + id
  const removeBotton = document.getElementById(removeID)
  if (done) {
    removeBotton.style = "display:inline-block"
  } else {
    removeBotton.style = "display:none"
  }
  const changeID = "change-" + id
  const changeCheckBox = document.getElementById(changeID)
  changeCheckBox.checked = done
  todos.forEach(todo => {
    if (todo.id === id) {
      todo.done = done
    }
  })
  updateTodoInfo()
}

// 按钮添加任务
input.addEventListener('keyup', function (e) {
  if (e.key === 'Enter') {
    const value = this.value.trim()
    if (!value) return

    todo = {
      id: `${Date.now()}`,
      name: value,
      done: false
    }

    todos.push(todo)

    addTodo(todo)
  
    this.value = ''
  }
})

allCheckBox.addEventListener('change', function (e) {
  console.log('e.target:', e.target)
  console.log('e.target:', e.target.checked) // todo 子项会造成联动,需提前保存其状态
  checked = e.target.checked
  todos.forEach(todo => {
    if (todo.done !== checked){
      changeTodo(todo.id, checked)
    }
  })
})


function handleTodoChange(target) {
  console.log('target:', target)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  const checked = target.checked
  changeTodo(todoID, checked)
}

function handleTodoDelete(target) {
  console.log('target:', target)
  console.log('target.parentNode:', target.parentNode)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  removeTodo(todoID)
}

本地保存示例

html
css
js

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Document</title>

  <link rel="stylesheet" href="index.css">
</head>

<body>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <div class="todo-header">
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
        <ul class="todo-main">
        </ul>
        <div class="todo-footer">
          <label>
            <input type="checkbox" />
          </label>
          <span>
            <span>已完成<span id="finish_num">0</span></span></span> / 全部<span id="all_num">0</span>
          </span>
          <button class="btn btn-danger">清除已完成任务</button>
        </div>
      </div>
    </div>
  </div>
  <script src="./index.js"></script>
</body>
</html>

/*base*/
body {
  background: #fff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}

const input = document.querySelector('.todo-header input')
const list = document.querySelector('.todo-main')
const allCheckBox = document.querySelector('.todo-footer input')
const finishNumSpan = document.querySelector('#finish_num')
const allNumSpan = document.querySelector('#all_num')

const todos = JSON.parse(localStorage.getItem("todos")) || []

// 返回todo 已完成,和总共的数量
function updateTodoInfo() {
  let finishNum = 0
  console.log('todos:', todos)
  todos.forEach(todo => {
    if (todo.done) {
      finishNum++
    }
  })
  console.log('finishNum:', finishNum)
  finishNumSpan.innerHTML = finishNum
  allNumSpan.innerHTML = todos.length

  allCheckBox.checked = finishNum === todos.length

  localStorage.setItem("todos", JSON.stringify(todos))
}

window.onload = function () {
  todos.forEach(todo => addTodo(todo))
}

// 添加任务 
/*
  @param {Object} todo: {
    id: string;
    name: string;
    done: boolean;
}
*/
function addTodo(todo) {
  const li = document.createElement('li')
  const checked = todo.done ? 'checked' : ''
  const display = todo.done ? 'inline-block' : 'none'
  li.id = todo.id

  li.innerHTML = `
    <label>
      <input id="change-${todo.id}" onchange="handleTodoChange(this)" type="checkbox" ${checked}/>
      <span>${todo.name}</span>
    </label>
    <button id="remove-${todo.id}" onclick="handleTodoDelete(this)" class="btn btn-danger" style="display:${display}">删除</button>
  `

  list.appendChild(li)

  updateTodoInfo()

}

function removeTodo(id) {
  const li = document.getElementById(id)
  console.log('li:', li)
  li.parentNode.removeChild(li)

  const index = todos.findIndex(todo => todo.id === id);

  if (index !== -1) {
    // 使用 splice() 方法删除找到的对象
    todos.splice(index, 1);
  }

  updateTodoInfo()
}

function changeTodo(id, done) {
  console.log(`in changeTodo id:${id}, done:${done}`)
  const removeID = "remove-" + id
  const removeBotton = document.getElementById(removeID)
  if (done) {
    removeBotton.style = "display:inline-block"
  } else {
    removeBotton.style = "display:none"
  }
  const changeID = "change-" + id
  const changeCheckBox = document.getElementById(changeID)
  changeCheckBox.checked = done
  todos.forEach(todo => {
    if (todo.id === id) {
      todo.done = done
    }
  })
  updateTodoInfo()
}

// 按钮添加任务
input.addEventListener('keyup', function (e) {
  if (e.key === 'Enter') {
    const value = this.value.trim()
    if (!value) return

    todo = {
      id: `${Date.now()}`,
      name: value,
      done: false
    }

    todos.push(todo)

    addTodo(todo)
  
    this.value = ''
  }
})

allCheckBox.addEventListener('change', function (e) {
  console.log('e.target:', e.target)
  console.log('e.target:', e.target.checked) // todo 子项会造成联动,需提前保存其状态
  checked = e.target.checked
  todos.forEach(todo => {
    if (todo.done !== checked){
      changeTodo(todo.id, checked)
    }
  })
})


function handleTodoChange(target) {
  console.log('target:', target)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  const checked = target.checked
  changeTodo(todoID, checked)
}

function handleTodoDelete(target) {
  console.log('target:', target)
  console.log('target.parentNode:', target.parentNode)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  removeTodo(todoID)
}

自定义事件示例

没必要,使用原生js 自定义事件处理比较麻烦

全局事件示例

同上

消息订阅和发布

html
css
js
js

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Document</title>

  <link rel="stylesheet" href="index.css">
  <script src="../../../../../lib/my/pubsub.js"></script>
</head>

<body>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <div class="todo-header">
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
        <ul class="todo-main">
        </ul>
        <div class="todo-footer">
          <label>
            <input type="checkbox" />
          </label>
          <span>
            <span>已完成<span id="finish_num">0</span></span></span> / 全部<span id="all_num">0</span>
          </span>
          <button class="btn btn-danger">清除已完成任务</button>
        </div>
      </div>
    </div>
  </div>
  <script src="index.js"></script>
</body>
</html>

/*base*/
body {
  background: #fff;
}

.btn {
  display: inline-block;
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}

// 非构建工具不支持这么使用, 只是为了代码提示加上,写完因屏蔽
// import '../../../../../lib/my/pubsub.js';

const input = document.querySelector('.todo-header input')
const list = document.querySelector('.todo-main')
const allCheckBox = document.querySelector('.todo-footer input')
const finishNumSpan = document.querySelector('#finish_num')
const allNumSpan = document.querySelector('#all_num')

const todos = JSON.parse(localStorage.getItem("todos")) || []

// 返回todo 已完成,和总共的数量
function updateTodoInfo() {
  let finishNum = 0
  console.log('todos:', todos)
  todos.forEach(todo => {
    if (todo.done) {
      finishNum++
    }
  })
  console.log('finishNum:', finishNum)
  finishNumSpan.innerHTML = finishNum
  allNumSpan.innerHTML = todos.length

  allCheckBox.checked = finishNum === todos.length

  localStorage.setItem("todos", JSON.stringify(todos))
}

window.onload = function () {
  todos.forEach(todo => addTodo(todo))
}

// 添加任务 
/*
  @param {Object} todo: {
    id: string;
    name: string;
    done: boolean;
}
*/
function addTodo(todo) {
  const li = document.createElement('li')
  const checked = todo.done ? 'checked' : ''
  const display = todo.done ? 'inline-block' : 'none'
  li.id = todo.id

  li.innerHTML = `
    <label>
      <input id="change-${todo.id}" onchange="handleTodoChange(this)" type="checkbox" ${checked}/>
      <span>${todo.name}</span>
    </label>
    <button id="remove-${todo.id}" onclick="handleTodoDelete(this)" class="btn btn-danger" style="display:${display}">删除</button>
  `

  list.appendChild(li)

  updateTodoInfo()

}

function removeTodo(id) {
  const li = document.getElementById(id)
  console.log('li:', li)
  li.parentNode.removeChild(li)

  const index = todos.findIndex(todo => todo.id === id);

  if (index !== -1) {
    // 使用 splice() 方法删除找到的对象
    todos.splice(index, 1);
  }

  updateTodoInfo()
}

function changeTodo(id, done) {
  console.log(`in changeTodo id:${id}, done:${done}`)
  const removeID = "remove-" + id
  const removeBotton = document.getElementById(removeID)
  if (done) {
    removeBotton.style = "display:inline-block"
  } else {
    removeBotton.style = "display:none"
  }
  const changeID = "change-" + id
  const changeCheckBox = document.getElementById(changeID)
  changeCheckBox.checked = done
  todos.forEach(todo => {
    if (todo.id === id) {
      todo.done = done
    }
  })
  updateTodoInfo()
}

// 按钮添加任务
input.addEventListener('keyup', function (e) {
  if (e.key === 'Enter') {
    const value = this.value.trim()
    if (!value) return

    const todo = {
      id: `${Date.now()}`,
      name: value,
      done: false
    }

    todos.push(todo)

    addTodo(todo)
  
    this.value = ''
  }
})

allCheckBox.addEventListener('change', function (e) {
  console.log('e.target:', e.target)
  console.log('e.target:', e.target.checked) // todo 子项会造成联动,需提前保存其状态
  checked = e.target.checked
  todos.forEach(todo => {
    if (todo.done !== checked){
      changeTodo(todo.id, checked)
    }
  })
})

eventCenter.subscribe('todo-change', (data) => {
  changeTodo(data.todoID, data.checked)
})

eventCenter.subscribe('todo-delete', (data) => {
  removeTodo(data.todoID)
})

function handleTodoChange(target) {
  console.log('target:', target)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  const checked = target.checked

  eventCenter.publish('todo-change', {
    todoID,
    checked
  })
}

function handleTodoDelete(target) {
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]

  eventCenter.publish('todo-delete', {
    todoID
  })
}

const eventCenter = (function () {
    /**
     * @type  {{[eventName: string]: Array<(param)=>void>}}
     */
    const subscribers = {};
    
    /**
     * @description: 
     * @param {string} eventName
     * @param {(param) => {}} callback
     * @return {*}
     */
    function subscribe(eventName, callback) {
        if (!subscribers[eventName]) {
            subscribers[eventName] = [];
        }
        subscribers[eventName].push(callback);
    }

    /**
     * @description: 
     * @param {string} eventName
     * @param {*} data
     * @return {*}
     */
    function publish(eventName, data) {
        if (Array.isArray(subscribers[eventName])) {
            subscribers[eventName].forEach(callback => callback(data));
        }
    }

    function unsubscribe(eventName, callback) {
        if (Array.isArray(subscribers[eventName])) {
            subscribers[eventName] = subscribers[eventName].filter(cb => cb !== callback);
        }
    }

    return {
        subscribe,
        publish,
        unsubscribe
    };
})();

编辑功能

html
css
js
js
js
js

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Document</title>

  <link rel="stylesheet" href="index.css">
  <script src="../../../../../lib/my/pubsub.js"></script>
</head>

<body>
  <div id="root">
    <div class="todo-container">
      <div class="todo-wrap">
        <div class="todo-header">
          <input type="text" placeholder="请输入你的任务名称,按回车键确认" />
        </div>
        <ul class="todo-main">
        </ul>
        <div class="todo-footer">
          <label>
            <input type="checkbox" />
          </label>
          <span>
            <span>已完成<span id="finish_num">0</span></span></span> / 全部<span id="all_num">0</span>
          </span>
        </div>
      </div>
    </div>
  </div>
  <script src="todoCentor.js"></script>
  <script src="elementCentor.js"></script>
  <script src="index.js"></script>
</body>
</html>

/*base*/
body {
  background: #fff;
}

.btn {
  /* display: inline-block; */
  padding: 4px 12px;
  margin-bottom: 0;
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  vertical-align: middle;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
  border-radius: 4px;
}

.btn-danger {
  color: #fff;
  background-color: #da4f49;
  border: 1px solid #bd362f;
}

.btn-edit {
  color: #fff;
  background-color: skyblue;
   border: 1px solid rgb(128, 202, 231);
}

.btn-danger:hover {
  color: #fff;
  background-color: #bd362f;
}

.btn:focus {
  outline: none;
}

.todo-container {
  width: 600px;
  margin: 0 auto;
}
.todo-container .todo-wrap {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

/*header*/
.todo-header input {
  width: 560px;
  height: 28px;
  font-size: 14px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 7px;
}

.todo-header input:focus {
  outline: none;
  border-color: rgba(82, 168, 236, 0.8);
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

/*main*/
.todo-main {
  margin-left: 0px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding: 0px;
}

.todo-empty {
  height: 40px;
  line-height: 40px;
  border: 1px solid #ddd;
  border-radius: 2px;
  padding-left: 5px;
  margin-top: 10px;
}
/*item*/
li {
  list-style: none;
  height: 36px;
  line-height: 36px;
  padding: 0 5px;
  border-bottom: 1px solid #ddd;
}

li label {
  float: left;
  cursor: pointer;
}

li label li input {
  vertical-align: middle;
  margin-right: 6px;
  position: relative;
  top: -1px;
}

li button {
  float: right;
  display: none;
  margin-top: 3px;
}

li:before {
  content: initial;
}

li:last-child {
  border-bottom: none;
}

li:hover button {
  display: block;
}

/*footer*/
.todo-footer {
  height: 40px;
  line-height: 40px;
  padding-left: 6px;
  margin-top: 5px;
}

.todo-footer label {
  display: inline-block;
  margin-right: 20px;
  cursor: pointer;
}

.todo-footer label input {
  position: relative;
  top: -1px;
  vertical-align: middle;
  margin-right: 5px;
}

.todo-footer button {
  float: right;
  margin-top: 5px;
}

const todoCentor = (function () {
  /**
   * @description: 获取初始todo
   * @returns {[{
   *  id: string;
   *  name: string;
   *  done: boolean;
   * }]}
   */
  function loadTodos() {
    return JSON.parse(localStorage.getItem("todos")) || []
  }

  /**
   * @description: 存储todos
   * @param {[{
   *  id: string;
   *  name: string;
   *  done: boolean;
   * }]} todos
   */
  function saveTodos(todos) {
    localStorage.setItem("todos", JSON.stringify(todos))
  }

  /**
   * @description: 改变todos中todo值
   * @param {[{
  *  id: string;
  *  name: string;
  *  done: boolean;
  * }]} todos
  * @param {{
  *  id: string;
  *  name: string;
  *  done: boolean;
  * }} todos todo
  */
  function addTodo(todos, todo) {
    todos.push(todo)
  }

  /**
   * @description: 
   * @param {[{
   *  id: string;
   *  name: string;
   *  done: boolean;
   * }]} todos
   * @param {string} todoID
   * @param {boolean} done
   * @return {*}
   */
  function updateTodoDone(todos, todoID, done) {
    todos.forEach(elem => {
      if (elem.id === todoID) {
        elem.done = done
      }
    })
  }

  /**
   * @description: 
   * @param {[{
  *  id: string;
  *  name: string;
  *  done: boolean;
  * }]} todos
  * @param {string} todoID
  * @param {string} name
  * @return {*}
  */
 function updateTodoName(todos, todoID, name) {
   todos.forEach(elem => {
     if (elem.id === todoID) {
       elem.name = name
     }
   })
 }

  /**
   * @description: 更新所有todo 的 done 状态
   * @param {[{
  *  id: string;
  *  name: string;
  *  done: boolean;
  * }]} todos
  * @param {boolean} done
  * @return {*}
  */
  function updateTodosDone(todos, done) {
    todos.forEach(elem => {
      if (elem.done !== done) {
        elem.done = done
      }
    })
  }

  /**
   * @description: 删除指定todo id
    * @param {[{
  *  id: string;
  *  name: string;
  *  done: boolean;
  * }]} todos
   * @param {string} id
   */
  function removeTodo(todos, id) {
    const index = todos.findIndex(todo => todo.id === id);

    if (index !== -1) {
      // 使用 splice() 方法删除找到的对象
      todos.splice(index, 1);
    }

    return todos
  }

  return {
    addTodo,
    loadTodos,
    saveTodos,
    updateTodoDone,
    updateTodoName,
    updateTodosDone,
    removeTodo,
  }
})()

// 控制 todo dom 元素
const elementCenter = (function () {

  const input = document.querySelector('.todo-header input')
  const list = document.querySelector('.todo-main')
  const allCheckBox = document.querySelector('.todo-footer input')
  const finishNumSpan = document.querySelector('#finish_num')
  const allNumSpan = document.querySelector('#all_num')

  // 按钮添加任务
  input.addEventListener('keyup', function (e) {
    if (e.key === 'Enter') {
      const value = this.value.trim()
      if (!value) return

      const todo = {
        id: `${Date.now()}`,
        name: value,
        done: false
      }

      addTodo(todo)
      eventCenter.publish('todo-add', todo)

      this.value = ''
    }
  })

  allCheckBox.addEventListener('change', function (e) {
    console.log('e.target:', e.target)
    console.log('e.target:', e.target.checked) // todo 子项会造成联动,需提前保存其状态
    checked = e.target.checked
    eventCenter.publish('update-todos-checked', checked)
  })

  /**
   * @description: 
   * @param {[{
   *  id: string;
   *  name: string;
   *  done: boolean;
   * }]} todos
   * @return {*}
   */
  function updateTodoInfos(todos) {

    // 更新底部所有
    const finishNum = todos.filter(todo => todo.done).length
    finishNumSpan.innerHTML = finishNum
    allNumSpan.innerHTML = todos.length
    if (finishNum == todos.length) {
      allCheckBox.checked = true
    }else{
      allCheckBox.checked = false
    }

    // 更新单个checked
    todos.forEach(todo => {
      const id = `change-${todo.id}`
      document.getElementById(id).checked = todo.done
    })
  }

  /**
   * @description: 添加todo元素
   * @param {{
   *  id: string;
   *  name: string;
   *  done: boolean;
   * }} todo
   * @return {*}
   */
  function addTodo(todo) {
    const li = document.createElement('li')
    const checked = todo.done ? 'checked' : ''
    li.id = todo.id

    li.innerHTML = `
      <label>
        <input id="change-${todo.id}" onchange="handleTodoChecked(this)" type="checkbox" ${checked}/>
        <span id="span-${todo.id}">${todo.name}</span>
        <input id="inputedit-${todo.id}" type="text" value="${todo.name}" onkeyup="handleTodeFinishEdit(event, this)" style="display:none"/>
      </label>
      <button id="remove-${todo.id}" onclick="handleTodoDelete(this)" class="btn btn-danger">删除</button>
      <button id="edit-${todo.id}" onclick="handleTodoEdit(this)" class="btn btn-edit">编辑</button>
    `

    list.appendChild(li)
  }

  /**
   * @description: 
   * @param {[{
   *  id: string;
   *  name: string;
   *  done: boolean;
   * }]} todos
   * @return {*}
   */
  function initTodos(todos) {
    todos.forEach(todo => {
      addTodo(todo)
    })
  }

  /**
   * @description: 
   * @param {string} todoID
   */
  function removeTodo(todoID) {
    const li = document.getElementById(todoID)
    li.parentNode.removeChild(li)
  }

  return {
    updateTodoInfos,
    initTodos,
    removeTodo,
  }
})();

/**
   * @description: 处理todo的change事件
   * @param {HTMLElement} target
   */
function handleTodoChecked(target) {
  console.log('target:', target)
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  const checked = target.checked

  eventCenter.publish('update-todo-checked', {
    todoID,
    checked
  })
}

/**
 * @description: 
 * @param {HTMLElement} target
 * @return {*}
 */
function handleTodoDelete(target) {
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]

  elementCenter.removeTodo(todoID)
  eventCenter.publish('todo-delete', todoID)
}

/**
 * @description: 
 * @param {HTMLElement} target
 * @return {*}
 */
function handleTodoEdit(target) {
  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]

  target.style = 'display:none'
  const spanID = `span-${todoID}`
  document.getElementById(spanID).style = 'display:none'

  const inputEditID = `inputedit-${todoID}`
  document.getElementById(inputEditID).style = 'display:inline-block'
}

/**
 * @description: 
 * @param {KeyboardEvent} event
 * @param {HTMLElement} target
 * @return {*}
 */
function handleTodeFinishEdit(event, target) {
  if (event.key !== 'Enter') return

  const todoIDs = target.id.split('-')
  const todoID = todoIDs[todoIDs.length - 1]
  const todoName = target.value

  const spanID = `span-${todoID}`
  document.getElementById(spanID).style = 'display:inline-block'
  document.getElementById(spanID).innerHTML = todoName

  target.style = 'display:none'
  const editID = `edit-${todoID}`
  document.getElementById(editID).style = ''

  eventCenter.publish('update-todo-name', {
    todoID,
    todoName
  })
}

const todos = todoCentor.loadTodos()

window.onload = function(){
  elementCenter.initTodos(todos)
  elementCenter.updateTodoInfos(todos)
}
// 改变todo的checked状态
eventCenter.subscribe('update-todo-checked', ({todoID, checked}) => {
  todoCentor.updateTodoDone(todos, todoID, checked)
  todoCentor.saveTodos(todos)
  elementCenter.updateTodoInfos(todos)
})

eventCenter.subscribe('update-todo-name', ({todoID, todoName}) => {
  todoCentor.updateTodoName(todos, todoID, todoName)
  todoCentor.saveTodos(todos)
  elementCenter.updateTodoInfos(todos)
})

// 改变所有todo的checked状态
eventCenter.subscribe('update-todos-checked', (checked) => {
  todoCentor.updateTodosDone(todos, checked)
  todoCentor.saveTodos(todos)
  elementCenter.updateTodoInfos(todos)
})

// 删除todo
eventCenter.subscribe('todo-delete', (todoID) => {
  todoCentor.removeTodo(todos, todoID)
  todoCentor.saveTodos(todos)
  elementCenter.updateTodoInfos(todos)
})

eventCenter.subscribe('todo-add', (todo) => {
  todoCentor.addTodo(todos, todo)
  todoCentor.saveTodos(todos)
  elementCenter.updateTodoInfos(todos)
})

eventCenter.subscribe('todo-edit', (todo) => {

})




const eventCenter = (function () {
    /**
     * @type  {{[eventName: string]: Array<(param)=>void>}}
     */
    const subscribers = {};
    
    /**
     * @description: 
     * @param {string} eventName
     * @param {(param) => {}} callback
     * @return {*}
     */
    function subscribe(eventName, callback) {
        if (!subscribers[eventName]) {
            subscribers[eventName] = [];
        }
        subscribers[eventName].push(callback);
    }

    /**
     * @description: 
     * @param {string} eventName
     * @param {*} data
     * @return {*}
     */
    function publish(eventName, data) {
        if (Array.isArray(subscribers[eventName])) {
            subscribers[eventName].forEach(callback => callback(data));
        }
    }

    function unsubscribe(eventName, callback) {
        if (Array.isArray(subscribers[eventName])) {
            subscribers[eventName] = subscribers[eventName].filter(cb => cb !== callback);
        }
    }

    return {
        subscribe,
        publish,
        unsubscribe
    };
})();

results matching ""

    No results matching ""