ES6的待办事项列表应用程序(todo list app with ES6)

Deadly 提交于 2020-03-02 20:14:25

ES6的待办事项列表应用程序

示例

在这里插入图片描述

CSS

@import url('https://fonts.googleapis.com/css?family=DM+Sans:400,500,700&display=swap');

* {
  box-sizing: border-box;
  outline: 0;
}

:root {
  --font: 'DM Sans', sans-serif;
}

body {
 background-image: linear-gradient( 102.7deg, rgba(253,218,255,1) 8.2%, rgba(223,173,252,1) 19.6%, rgba(173,205,252,1) 36.8%, rgba(173,252,244,1) 73.2%, rgba(202,248,208,1) 90.9% );
  background-attachment: fixed;
  display: flex;
  flex-direction: column;
  background-repeat: no-repeat;
  background-size: cover;
  padding: 20px;
  height: 100vh;
  overflow: hidden;
}

.app {
  max-width: 400px;
  width: 100%;
  margin: auto;
  background-color: #fff;
  font-family: var(--font);
  border-radius: 16px;
  font-size: 15px;
  overflow: hidden;
  color: #455963;
  box-shadow: 0 20px 80px rgba(0,0,0,.3);
}

.task-list {
  max-height: 60vh;
  overflow: auto;
}

.task-status {
  appearance: none;
  width: 18px;
  height: 18px;
  cursor: pointer;
  border: 2px solid #bbbdc7;
  border-radius: 50%;
  background-color: #fff;
  margin-right: 10px;
  position: relative;
}

.task-status:checked {
  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='405.272' height='405.272'%3e%3cpath d='M393.401 124.425L179.603 338.208c-15.832 15.835-41.514 15.835-57.361 0L11.878 227.836c-15.838-15.835-15.838-41.52 0-57.358 15.841-15.841 41.521-15.841 57.355-.006l81.698 81.699L336.037 67.064c15.841-15.841 41.523-15.829 57.358 0 15.835 15.838 15.835 41.514.006 57.361z' fill='%23fff'/%3e%3c/svg%3e");
  background-size: 10px;
  background-color: #4acea3;
  border-color: #38bb90;
  background-repeat: no-repeat;
  background-position: center;
}

.task-delete {
  margin-left: 10px;
}

.task-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  padding: 12px 20px;
}

.task-item + .task-item {
  border-top: 1px solid #eef0f5;
}

.task-item:hover {
  background-color: #f6fbff;
}

.task-name {
  margin-right: auto;
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.task-item.is-completed > .task-name {
  text-decoration: line-through wavy rgba(0,0,0,.3);
}

.task-item.is-completed {
  background-color: rgba(74, 206, 163, 0.1);
}

.task-header-title {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  padding: 20px 20px 6px 20px;
}

.task-tools {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  align-items: flex-start;
  padding: 0 20px;
}

.task-filter {
  border: 0;
  padding: 3px 8px;
  background: 0;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  font-family: var(--font);
  color: #8a9ca5;
  border-radius: 20px;
}

.task-filter.is-active {
  background-color: #7996a5;
  color: #fff;
}

.task-count {
  color: #8a9ca5;
  font-size: 14px;
}

.task-form {
  display: flex;
  margin-top: 10px;
}

.task-input {
  flex: 1;
  font-size: 16px;
  font-family: var(--font);
  padding: 10px 20px;
  border: 0;
  box-shadow: 0 -1px 0 #e2e4ea inset;
  color: #455963;
}

.task-input::placeholder {
  color: #a8b5bb;
}

.task-input:focus {
  box-shadow: 0 -1px 0 #bdcdd6 inset;
}

.task-button { display: none; }

.task-delete {
  border: 0;
  width: 18px;
  height: 18px;
  padding: 0;
  overflow: hidden;
  background-color: transparent;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23dc4771' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 174.239 174.239'%3e%3cpath d='M87.12 0C39.082 0 0 39.082 0 87.12s39.082 87.12 87.12 87.12 87.12-39.082 87.12-87.12S135.157 0 87.12 0zm0 159.305c-39.802 0-72.185-32.383-72.185-72.185S47.318 14.935 87.12 14.935s72.185 32.383 72.185 72.185-32.384 72.185-72.185 72.185z'/%3e%3cpath d='M120.83 53.414c-2.917-2.917-7.647-2.917-10.559 0L87.12 76.568 63.969 53.414c-2.917-2.917-7.642-2.917-10.559 0s-2.917 7.642 0 10.559l23.151 23.153-23.152 23.154a7.464 7.464 0 000 10.559 7.445 7.445 0 005.28 2.188 7.437 7.437 0 005.28-2.188L87.12 97.686l23.151 23.153a7.445 7.445 0 005.28 2.188 7.442 7.442 0 005.28-2.188 7.464 7.464 0 000-10.559L97.679 87.127l23.151-23.153a7.465 7.465 0 000-10.56z'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-size: cover;
  cursor: pointer;
  display: none;
}

.task-item:hover > .task-delete {
  display: block;
}

.task-empty {
  height: 120px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='%23f4f4f4' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 486.463 486.463'%3e%3cpath d='M243.225 333.382c-13.6 0-25 11.4-25 25s11.4 25 25 25c13.1 0 25-11.4 24.4-24.4.6-14.3-10.7-25.6-24.4-25.6z'/%3e%3cpath d='M474.625 421.982c15.7-27.1 15.8-59.4.2-86.4l-156.6-271.2c-15.5-27.3-43.5-43.5-74.9-43.5s-59.4 16.3-74.9 43.4l-156.8 271.5c-15.6 27.3-15.5 59.8.3 86.9 15.6 26.8 43.5 42.9 74.7 42.9h312.8c31.3 0 59.4-16.3 75.2-43.6zm-34-19.6c-8.7 15-24.1 23.9-41.3 23.9h-312.8c-17 0-32.3-8.7-40.8-23.4-8.6-14.9-8.7-32.7-.1-47.7l156.8-271.4c8.5-14.9 23.7-23.7 40.9-23.7 17.1 0 32.4 8.9 40.9 23.8l156.7 271.4c8.4 14.6 8.3 32.2-.3 47.1z'/%3e%3cpath d='M237.025 157.882c-11.9 3.4-19.3 14.2-19.3 27.3.6 7.9 1.1 15.9 1.7 23.8 1.7 30.1 3.4 59.6 5.1 89.7.6 10.2 8.5 17.6 18.7 17.6s18.2-7.9 18.7-18.2c0-6.2 0-11.9.6-18.2 1.1-19.3 2.3-38.6 3.4-57.9.6-12.5 1.7-25 2.3-37.5 0-4.5-.6-8.5-2.3-12.5-5.1-11.2-17-16.9-28.9-14.1z'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: center;
  font-weight: 500;
  font-size: 18px;
  background-size: 80px;
}

@media (max-width: 600px) {
  .task-delete {
    display: block;
  }
}

JS

class Todo {
  constructor({
    title = "Todo App",
    data = [],
    onAdded = () => {},
    onDeleted = () => {},
    onStatusChanged = () => {}
  } = {}) {
    this.nodes = {};
    this.title = title;
    this.data = data;
    this.filteredData = data;
    this.count = data.length;
    this.addTask = this.addTask.bind(this);
    this.deleteTask = this.deleteTask.bind(this);
    this.toggleStatus = this.toggleStatus.bind(this);
    this.filterData = this.filterData.bind(this);
    this.onAdded = onAdded;
    this.onDeleted = onDeleted;
    this.onStatusChanged = onStatusChanged;

    this.filterTypes = [
      {
        name: "All",
        queryParam: null,
        queryValue: null,
        active: true
      },
      {
        name: "Active",
        queryParam: "completed",
        queryValue: false,
        active: false
      },
      {
        name: "Completed",
        queryParam: "completed",
        queryValue: true,
        active: false
      }
    ];

    this.elementDefaults = {
      type: "div",
      markup: "",
      container: document.body,
      attributes: {},
      events: {}
    };
  }

  elementCreator(options) {
    const config = { ...this.elementDefaults, ...options };
    const elementNode = document.createElement(config.type);

    Object.keys(config.attributes).forEach(a => {
      config.attributes[a] !== null &&
        elementNode.setAttribute(a, config.attributes[a]);
    });

    elementNode.innerHTML = config.markup;
    config.container.append(elementNode);

    Object.keys(config.events).forEach(e => {
      this.eventBinder(
        elementNode,
        e,
        config.events[e].action,
        config.events[e].api
      );
    });

    return elementNode;
  }

  updateCount() {
    this.count = this.data.length;
    this.nodes.count.innerHTML =
      this.count > 1 ? `${this.count} tasks` : `${this.count} task`;
  }

  eventBinder(el, event, action, api = false) {
    el.addEventListener(event, e => {
      api ? action(e) : action();
    });
  }

  emptyListUI(message = "Not found a task") {
    this.nodes.list.innerHTML = "";
    this.nodes.emptyList = this.elementCreator({
      markup: message,
      attributes: {
        class: "task-empty"
      },
      container: this.nodes.list
    });
  }

  addTask({
    id = new Date().getUTCMilliseconds(),
    name = `New task #${new Date().getUTCMilliseconds()}`,
    completed = false
  } = {}) {
    const inputValue = this.nodes.input.value.trim();
    const taskName = inputValue.length > 0 ? inputValue : name;
    const newTask = { id, name: taskName, completed };

    this.nodes.input.value = "";
    this.data.push(newTask);
    this.listUI(this.data);
    this.onAdded(newTask);
    this.updateCount();
    this.filterData();
  }

  filterData(e, param = null, value = null) {
    const attrParam = e ? e.target.getAttribute("data-param") : null;
    const attrValue = e ? e.target.getAttribute("data-value") : null;

    const queryParam = param ? param : attrParam;
    const queryValue = value ? value : attrValue;

    this.filteredData =
      !queryValue && !queryParam
        ? this.data
        : this.data.filter(task => String(task[queryParam]) === queryValue);
    this.listUI(this.filteredData);

    const filterTypes = this.filterTypes.map(filter => {
      filter.active =
        String(filter.queryParam) === String(queryParam) &&
        String(filter.queryValue) === String(queryValue);
      return filter;
    });

    this.filterUI(filterTypes);
    this.filterTypes = filterTypes;
  }

  toggleStatus(e, id = null) {
    const taskId = id ? id : Number(e.target.getAttribute("data-id"));
    const updatedData = this.data.map(task => {
      if (task.id === taskId) task.completed = !task.completed;
      return task;
    });

    this.listUI(updatedData);
    this.data = updatedData;
    this.onStatusChanged(taskId);
    this.filterData();
  }

  deleteTask(e, id = null) {
    const taskId = id ? id : Number(e.target.getAttribute("data-id"));
    const updatedData = this.data.filter(task => task.id !== taskId);

    this.listUI(updatedData);
    this.data = updatedData;
    this.onDeleted(taskId);
    this.updateCount();
    this.filterData();
  }

  generalUI() {
    this.nodes.app = this.elementCreator({
      attributes: {
        class: "app"
      }
    });

    this.nodes.header = this.elementCreator({
      attributes: {
        class: "task-header"
      },
      container: this.nodes.app
    });

    this.nodes.title = this.elementCreator({
      type: "h1",
      markup: this.title,
      attributes: {
        class: "task-header-title"
      },
      container: this.nodes.header
    });

    this.nodes.list = this.elementCreator({
      attributes: {
        class: "task-list"
      },
      container: this.nodes.app
    });

    this.nodes.tools = this.elementCreator({
      attributes: {
        class: "task-tools"
      },
      container: this.nodes.header
    });

    this.nodes.form = this.elementCreator({
      type: "form",
      attributes: {
        class: "task-form"
      },
      events: {
        submit: { action: e => e.preventDefault(), api: true }
      },
      container: this.nodes.header
    });

    this.nodes.count = this.elementCreator({
      markup: this.count > 1 ? `${this.count} tasks` : `${this.count} task`,
      attributes: {
        class: "task-count"
      },
      container: this.nodes.tools
    });

    this.nodes.filters = this.elementCreator({
      attributes: {
        class: "task-filters"
      },
      container: this.nodes.tools
    });
  }

  formUI() {
    this.nodes.input = this.elementCreator({
      type: "input",
      attributes: {
        class: "task-input",
        placeholder: "Add a new task...",
        autofocus: "true"
      },
      container: this.nodes.form
    });

    this.nodes.button = this.elementCreator({
      type: "button",
      markup: "Add Task",
      attributes: {
        class: "task-button"
      },
      events: {
        click: { action: this.addTask, api: false }
      },
      container: this.nodes.form
    });
  }

  filterUI(filterTypes = this.filterTypes) {
    this.nodes.filters.innerHTML = "";

    filterTypes.forEach(type => {
      const button = this.elementCreator({
        type: "button",
        markup: type.name,
        attributes: {
          class: `task-filter${type.active ? " is-active" : ""}`,
          "data-param": type.queryParam !== undefined ? type.queryParam : null,
          "data-value": type.queryValue !== undefined ? type.queryValue : null
        },
        events: {
          click: { action: this.filterData, api: true }
        },
        container: this.nodes.filters
      });
    });
  }

  listUI(data = this.data) {
    this.nodes.list.innerHTML = "";

    if (data.length === 0) {
      this.emptyListUI();
      return;
    }

    data.forEach(task => {
      const item = this.elementCreator({
        attributes: {
          class: `task-item${task.completed ? " is-completed" : ""}`
        },
        container: this.nodes.list
      });

      const checkbox = this.elementCreator({
        type: "input",
        attributes: {
          class: "task-status",
          type: "checkbox",
          checked: task.completed ? task.completed : null,
          "data-id": task.id
        },
        events: {
          change: { action: this.toggleStatus, api: true }
        },
        container: item
      });

      const name = this.elementCreator({
        type: "label",
        markup: task.name,
        attributes: {
          class: "task-name"
        },
        container: item
      });

      const button = this.elementCreator({
        type: "button",
        markup: "",
        attributes: {
          class: "task-delete",
          "data-id": task.id
        },
        events: {
          click: { action: this.deleteTask, api: true }
        },
        container: item
      });
    });
  }

  init() {
    this.generalUI();
    this.formUI();
    this.listUI();
    this.filterUI();
  }
}

const todoList = [
  {
    id: -1,
    name: "Morning walk",
    completed: true
  },
  {
    id: -2,
    name: "Meeting with Holden Caulfield",
    completed: true
  },
  {
    id: -3,
    name: "Call Alper Kamu",
    completed: false
  },
  {
    id: -4,
    name: "Book flight to Hungary",
    completed: false
  },
  {
    id: -5,
    name: "Blog about CSS box model",
    completed: true
  }
];

const TodoApp = new Todo({
  title: new Date().toDateString(),
  data: todoList
});

TodoApp.init();

// Please open the console to see changes
TodoApp.onAdded = task => console.log("Added", task);
TodoApp.onDeleted = id => console.log("Deleted, id: ", id);
TodoApp.onStatusChanged = id => console.log("Status changed, id:", id);

// Add Task
TodoApp.addTask({ id: -6 });

// Delete Task
TodoApp.deleteTask(null, -6);

// Toggle Status
TodoApp.toggleStatus(null, -5);

// Filter Data
// TodoApp.filterData(null, "completed", "true");
更多有趣示例 尽在 小红砖社区https://xhz.bos.xyz
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!