From fe3931110d174cbb77739a60c44c9269d618172a Mon Sep 17 00:00:00 2001 From: Jiro Date: Mon, 22 Apr 2024 19:52:07 -0700 Subject: [PATCH] [0300-js-dom-todo] add sources to global public --- public/0300-js-dom-todo/index.html | 56 ++++++ public/0300-js-dom-todo/js/function.js | 178 ++++++++++++++++++ public/0300-js-dom-todo/js/lib.js | 98 ++++++++++ public/0300-js-dom-todo/stylesheets/style.css | 1 + public/index.html | 2 +- 5 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 public/0300-js-dom-todo/index.html create mode 100644 public/0300-js-dom-todo/js/function.js create mode 100644 public/0300-js-dom-todo/js/lib.js create mode 100644 public/0300-js-dom-todo/stylesheets/style.css diff --git a/public/0300-js-dom-todo/index.html b/public/0300-js-dom-todo/index.html new file mode 100644 index 0000000..db68d92 --- /dev/null +++ b/public/0300-js-dom-todo/index.html @@ -0,0 +1,56 @@ + + + + Todo + + + + + + + + + +
+
+
+

Todo

+
+
+
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+
 
+
タスク
+
期限日
+
 
+
+
+
+
+
+
+
+ + + + + diff --git a/public/0300-js-dom-todo/js/function.js b/public/0300-js-dom-todo/js/function.js new file mode 100644 index 0000000..bc05ccc --- /dev/null +++ b/public/0300-js-dom-todo/js/function.js @@ -0,0 +1,178 @@ + +const state = { + showCompleted: false, + tasks: [ + { + name: 'Task 1', + deadline: new AppDate().getDateInXMonth(1), + }, + { + name: 'Task 2', + deadline: new AppDate().getDateInXMonth(2), + }, + { + name: 'Task 3', + deadline: new AppDate().getDateInXMonth(3), + }, + ] +} + +function renderTasks(container) { + const { tasks, showCompleted } = state + + container.innerHTML = '' + tasks.sort((a, b) => a.deadline.getTime() - b.deadline.getTime()) + const rerender = () => renderTasks(container) + + tasks.forEach((task, index) => { + if (!showCompleted && task.completed) { + return + } + + renderTask(container, task, { + onEditName: (col) => { + const value = col.textContent + col.innerHTML = '' + + const input = document.createElement('input') + input.setAttribute('type', 'text') + input.setAttribute('class', 'form__input-field') + input.value = value + + col.innerHTML = '' + col.appendChild(input) + + input.addEventListener('blur', (e) => { + e.stopPropagation() + if (e.target.value) { + const name = e.target.value + task.name = name + } + rerender() + }) + + input.focus() + }, + onEditDeadline: (col) => { + col.innerHTML = '' + + const input = document.createElement('input') + input.setAttribute('type', 'date') + input.setAttribute('class', 'form__input-field') + input.value = col.textContent + + col.innerHTML = '' + col.appendChild(input) + + input.addEventListener('blur', (e) => { + e.stopPropagation() + if (e.target.value) { + task.deadline = AppDate.parse(e.target.value) + } + + rerender() + }) + + input.focus() + input.showPicker() + }, + onComplete: (row, completed) => { + row.classList.toggle('list__item--completed') + if (!showCompleted && completed) { + row.classList.add('list__item--completed-dismissing') + } + task.completed = completed + + setTimeout(() => { + rerender() + }, 1000) + }, + onDelete: () => { + tasks.splice(index, 1) + rerender() + } + }) + }) +} + +const buildColumns = () => { + return { + checkbox: div('list__item-col list__item-col--checkbox'), + name: div('list__item-col list__item-col--name'), + deadline: div('list__item-col list__item-col--deadline'), + actions: div('list__item-col list__item-col--actions') + } +} + +function renderTask(target, task, { onEditName, onEditDeadline, onComplete, onDelete }) { + const li = document.createElement('li') + const taskContainer = div('list__item') + + const columns = buildColumns() + + const checkboxEle = checkbox(task.completed, (checked) => { + onComplete(taskContainer, checked) + }) + + columns.checkbox.appendChild(checkboxEle) + columns.name.textContent = task.name + columns.name.addEventListener('click', () => { + onEditName(columns.name) + }) + columns.deadline.textContent = task.deadline.toString() + columns.deadline.addEventListener('click', () => { + onEditDeadline(columns.deadline) + }) + + columns.actions.appendChild(icon('icon icon--trash fa-solid fa-trash', () => { + if (window.confirm('このタスクを削除しますか?')) { + onDelete() + } + })) + + Object.values(columns).forEach((column) => taskContainer.appendChild(column)) + li.appendChild(taskContainer) + + target.appendChild(li) +} + +function onSubmitTask(container) { + const form = document.querySelector('.js-form') + const data = new FormData(form) + const name = data.get('name') + if (!name) { + window.alert('タスク名を入力してください。') + return + } + + const deadline = AppDate.parse(data.get('deadline')) + if (!deadline) { + window.alert('期限日を入力してください。') + return + } + + state.tasks.push({ + name, + deadline + }) + + renderTasks(container) + form.reset() +} + +function main() { + const todoContainer = document.querySelector('.js-list-container') + + document.querySelector('.js-form').addEventListener('submit', (e) => { + e.preventDefault() + onSubmitTask(todoContainer) + }) + + document.querySelector('.js-show-completed').addEventListener('change', (e) => { + state.showCompleted = e.target.checked + renderTasks(todoContainer) + }) + renderTasks(todoContainer) +} + +main() diff --git a/public/0300-js-dom-todo/js/lib.js b/public/0300-js-dom-todo/js/lib.js new file mode 100644 index 0000000..d41ecec --- /dev/null +++ b/public/0300-js-dom-todo/js/lib.js @@ -0,0 +1,98 @@ + +// date class +class AppDate { + static parse(dateString) { + if (!dateString) { + return + } + + const [year, month, day] = dateString.split('-').map((str) => parseInt(str, 10)) + return new AppDate(new Date(year, month - 1, day)) + } + + constructor(date = new Date()) { + this.date = date + } + + get cloneDate() { + return new Date(this.date.getTime()) + } + + toString() { + const month = (this.date.getMonth() + 1).toString().padStart(2, '0') + const day = this.date.getDate().toString().padStart(2, '0') + + return `${this.date.getFullYear()}-${month}-${day}` + } + + getDateInXMonth(n) { + const date = (this.date.getMonth() + n) % 12 + const res = new Date(this.cloneDate.setMonth(date)) + + return new AppDate(res) + } + + getTime() { + return this.date.getTime() + } + + isAfter(date) { + return this.date.getTime() > date.getTime() + } +} + +// components + +function div(klass) { + const div = document.createElement('div') + div.setAttribute('class', klass) + + return div +} + +function icon(klass, onClick) { + const i = document.createElement('i') + i.setAttribute('class', klass) + i.addEventListener('click', onClick) + + return i +} + +function button(text, klass, onClick) { + const button = document.createElement('button') + button.setAttribute('class', `button ${klass}`) + button.textContent = text + button.addEventListener('click', onClick) + + return button +} + +function checkbox(checked, onClick) { + const label = document.createElement('label') + label.setAttribute('class', 'checkbox') + if (checked) { + label.classList.add('checkbox--checked') + } + + const checkbox = document.createElement('input') + checkbox.setAttribute('type', 'checkbox') + checkbox.setAttribute('class', 'checkbox__input') + checkbox.checked = checked + + label.addEventListener('click', () => { + checkbox.checked = !checkbox.checked + if (checkbox.checked) { + label.classList.add('checkbox--checked') + } else { + label.classList.remove('checkbox--checked') + } + + onClick(checkbox.checked) + }) + label.appendChild(checkbox) + label.appendChild(icon('icon icon--check fa-solid fa-check', onClick)) + + return label +} + + diff --git a/public/0300-js-dom-todo/stylesheets/style.css b/public/0300-js-dom-todo/stylesheets/style.css new file mode 100644 index 0000000..a316db4 --- /dev/null +++ b/public/0300-js-dom-todo/stylesheets/style.css @@ -0,0 +1 @@ +.body{font-family:"Inter","Noto Sans JP","Open Sans",sans-serif}.body{box-sizing:border-box;margin:0}.main__header{padding:16px 32px;border-bottom:1px solid #f0f0f0}.main__header-title{font-size:16px;margin:0}.main__body{max-width:1200px;margin:32px auto}.list{padding:32px}.list__container{list-style:none;padding:0}.list__setting{padding:16px;display:flex;justify-content:flex-end}.list__header{display:flex;align-items:center;border-bottom:1px solid #f0f0f0}.list__header-item{flex:1;padding:16px;border-right:1px solid #f0f0f0;font-size:12px}.list__header-item:last-child{border-right:0}.list__header-item--name{flex:4}.list__item{display:flex;align-items:center;width:100%;border-bottom:1px solid #f0f0f0;transition:opacity .8s}.list__item--completed-dismissing{opacity:0}.list__item-col{box-sizing:border-box;flex:1;padding:16px;border-right:1px solid #f0f0f0}.list__item-col--name{cursor:pointer;flex:4}.list__item-col--deadline{cursor:pointer}.list__item-col--checkbox{display:flex;justify-content:center}.list__item-col--actions{font-size:12px;display:flex;justify-content:center}.button{border:0;cursor:pointer;border-radius:8px;background:#fff;padding:8px 16px;min-width:200px}.button--primary{background:#7a72ff;color:#fefefe}.button--danger{background:#d64045;color:#fefefe}.checkbox{display:block;border-radius:50%;border:1px solid #c6c6c6;padding:4px;height:16px;width:16px;cursor:pointer;color:#fefefe;display:flex;justify-content:center;align-items:center}.checkbox--checked{background:#7a72ff}.checkbox__input{border:0;appearance:none;position:absolute}.form{padding:16px;border:1px solid #f0f0f0;border-radius:8px}.form__input{padding:16px}.form__input-field{box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #f0f0f0;width:100%}.form__input-horizontal-group{display:flex;width:100%}.form__input-group-item{flex:1;padding:16px}.form__input-footer{padding:16px;display:flex;justify-content:flex-end}.form__input-label{display:block;font-size:12px;margin-bottom:8px}.icon{cursor:pointer;color:#666}.icon--check{color:#fefefe}/*# sourceMappingURL=style.css.map */ diff --git a/public/index.html b/public/index.html index 686082a..1ae453a 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,7 @@

フロントエンド課題一覧

  1. HTML/CSS(BEM, Sass)課題
  2. JavaScript 課題
  3. -
  4. React 入門課題
  5. +
  6. React 入門課題
  7. React + Context API + Next.js 課題
  8. TypeScript 課題
  9. React + HTTP 課題