From 77238353f95e7e6e29a844c19d6c2a169dd35e4b Mon Sep 17 00:00:00 2001 From: Morris Brodersen Date: Tue, 10 May 2022 12:47:17 +0200 Subject: [PATCH] add date picker, refactor event names, cleanups --- README.md | 1 + package-lock.json | 38 +++---- package.json | 2 +- public/index.html | 2 + public/scripts/AppDatePicker.js | 158 ++++++++++++++++++++++++++++++ public/scripts/TodoApp.js | 6 +- public/scripts/TodoCustomList.js | 8 +- public/scripts/TodoDay.js | 4 +- public/scripts/TodoFrameCustom.js | 6 +- public/scripts/TodoFrameDays.js | 60 ++++++++++-- public/scripts/TodoItem.js | 6 +- public/scripts/TodoItemInput.js | 2 +- public/scripts/TodoList.js | 2 +- public/scripts/TodoStore.js | 30 +++--- public/styles/app-button.css | 9 ++ public/styles/app-date-picker.css | 51 ++++++++++ public/styles/todo-app.css | 3 + public/styles/todo-frame.css | 6 +- 18 files changed, 334 insertions(+), 60 deletions(-) create mode 100644 public/scripts/AppDatePicker.js create mode 100644 public/styles/app-date-picker.css create mode 100644 public/styles/todo-app.css diff --git a/README.md b/README.md index b9b0a94..1c2b726 100644 --- a/README.md +++ b/README.md @@ -804,6 +804,7 @@ Thanks! - Refactored for event-driven communication exclusively - Moved original ES5-based version of the study to [/es5](./es5) - Added assessment regarding library development +- Added date picker ### 01/2021 diff --git a/package-lock.json b/package-lock.json index 2597826..141a206 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,19 +89,19 @@ } }, "@eslint/eslintrc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", - "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", + "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", + "espree": "^9.3.2", "globals": "^13.9.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, @@ -532,12 +532,12 @@ "dev": true }, "eslint": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", - "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", + "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.2", + "@eslint/eslintrc": "^1.2.3", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -548,7 +548,7 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.3.2", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -564,7 +564,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -624,13 +624,13 @@ "dev": true }, "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", "dev": true, "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" } }, @@ -882,9 +882,9 @@ } }, "globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.14.0.tgz", + "integrity": "sha512-ERO68sOYwm5UuLvSJTY7w7NP2c8S4UcXs3X1GBX8cwOr+ShOcDBbCY5mH4zxz0jsYCdJ8ve8Mv9n2YGJMB1aeg==", "dev": true, "requires": { "type-fest": "^0.20.2" diff --git a/package.json b/package.json index e9c2f2b..02d3040 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "homepage": "https://github.com/morris/vanilla-todo#readme", "devDependencies": { - "eslint": "^8.14.0", + "eslint": "^8.15.0", "eslint-plugin-compat": "^4.0.2", "http-server": "^14.1.0", "prettier": "^2.6.2", diff --git a/public/index.html b/public/index.html index 8604fef..e3d4925 100644 --- a/public/index.html +++ b/public/index.html @@ -12,9 +12,11 @@ + + diff --git a/public/scripts/AppDatePicker.js b/public/scripts/AppDatePicker.js new file mode 100644 index 0000000..5ab8453 --- /dev/null +++ b/public/scripts/AppDatePicker.js @@ -0,0 +1,158 @@ +import { AppIcon } from './AppIcon.js'; +import { formatMonth } from './util.js'; + +const datesCell = ` + +`; + +const datesRow = ` + + ${datesCell} + ${datesCell} + ${datesCell} + ${datesCell} + ${datesCell} + ${datesCell} + ${datesCell} + +`; + +export function AppDatePicker(el) { + const now = new Date(); + const state = { + year: now.getFullYear(), + month: now.getMonth() + 1, + show: false, + }; + + el.innerHTML = ` +

+ + + +

+ + + + + + + + + + + + + + ${datesRow} + ${datesRow} + ${datesRow} + ${datesRow} + ${datesRow} + + + `; + + el.querySelectorAll('.app-icon').forEach(AppIcon); + + el.addEventListener('toggleDatePicker', (e) => + update({ show: e.detail ?? !state.show }) + ); + + el.addEventListener('setMonth', (e) => update(e.detail)); + + el.querySelector('.previousmonth').addEventListener('click', previousMonth); + el.querySelector('.nextmonth').addEventListener('click', nextMonth); + + el.addEventListener('click', (e) => { + if (!e.target.matches('.app-button')) return; + + update({ show: false }); + + el.dispatchEvent( + new CustomEvent('pickDate', { + detail: new Date( + e.target.dataset.year, + e.target.dataset.month - 1, + e.target.dataset.day + ), + bubbles: true, + }) + ); + }); + + function previousMonth() { + update( + state.month > 1 + ? { + year: state.year, + month: state.month - 1, + } + : { + year: state.year - 1, + month: 12, + } + ); + } + + function nextMonth() { + update( + state.month < 12 + ? { + year: state.year, + month: state.month + 1, + } + : { + year: state.year + 1, + month: 1, + } + ); + } + + function update(next) { + Object.assign(state, next); + + el.classList.toggle('-show', state.show); + + const now = new Date(); + const first = new Date(state.year, state.month - 1, 1); + + el.querySelector('.month').innerHTML = `${formatMonth( + first + )} ${first.getFullYear()}`; + + let current = new Date(first); + current.setDate(1 - current.getDay()); + + const datesBody = el.querySelector('.dates > tbody'); + + for (let i = 0; i < 35; ++i) { + const row = Math.floor(i / 7); + const column = i % 7; + + const cell = datesBody.children[row].children[column]; + const button = cell.children[0]; + + button.innerHTML = current.getDate(); + + button.dataset.year = current.getFullYear(); + button.dataset.month = current.getMonth() + 1; + button.dataset.day = current.getDate(); + + button.classList.toggle( + '-highlight', + current.getFullYear() === now.getFullYear() && + current.getMonth() === now.getMonth() && + current.getDate() === now.getDate() + ); + + current.setDate(current.getDate() + 1); + } + } + + update(); +} diff --git a/public/scripts/TodoApp.js b/public/scripts/TodoApp.js index ca8a822..f2eb4e2 100644 --- a/public/scripts/TodoApp.js +++ b/public/scripts/TodoApp.js @@ -23,7 +23,9 @@ export function TodoApp(el) {

- +

@@ -101,7 +103,7 @@ export function TodoApp(el) { el.addEventListener('draggableCancel', flip); el.addEventListener('draggableDrop', flip); - el.dispatchEvent(new CustomEvent('loadStore')); + el.dispatchEvent(new CustomEvent('loadTodoStore')); function update(next) { Object.assign(state, next); diff --git a/public/scripts/TodoCustomList.js b/public/scripts/TodoCustomList.js index 80946bc..dfdade0 100644 --- a/public/scripts/TodoCustomList.js +++ b/public/scripts/TodoCustomList.js @@ -76,7 +76,7 @@ export function TodoCustomList(el) { } el.dispatchEvent( - new CustomEvent('deleteList', { + new CustomEvent('deleteTodoList', { detail: state.list, bubbles: true, }) @@ -100,11 +100,11 @@ export function TodoCustomList(el) { }); }); - el.addEventListener('addItem', (e) => { + el.addEventListener('addTodoItem', (e) => { e.detail.listId = state.list.id; }); - el.addEventListener('moveItem', (e) => { + el.addEventListener('moveTodoItem', (e) => { e.detail.listId = state.list.id; e.detail.index = e.detail.index ?? 0; }); @@ -113,7 +113,7 @@ export function TodoCustomList(el) { function save() { el.dispatchEvent( - new CustomEvent('saveList', { + new CustomEvent('saveTodoList', { detail: { list: state.list, title: inputEl.value.trim() }, bubbles: true, }) diff --git a/public/scripts/TodoDay.js b/public/scripts/TodoDay.js index 5392ce7..c54c74a 100644 --- a/public/scripts/TodoDay.js +++ b/public/scripts/TodoDay.js @@ -17,11 +17,11 @@ export function TodoDay(el) { TodoList(el.querySelector('.todo-list')); - el.addEventListener('addItem', (e) => { + el.addEventListener('addTodoItem', (e) => { e.detail.listId = state.dateId; }); - el.addEventListener('moveItem', (e) => { + el.addEventListener('moveTodoItem', (e) => { e.detail.listId = state.dateId; e.detail.index = e.detail.index ?? 0; }); diff --git a/public/scripts/TodoFrameCustom.js b/public/scripts/TodoFrameCustom.js index d5e64a2..9ea43e5 100644 --- a/public/scripts/TodoFrameCustom.js +++ b/public/scripts/TodoFrameCustom.js @@ -31,13 +31,13 @@ export function TodoFrameCustom(el) { el.querySelector('.back').addEventListener('click', () => { el.dispatchEvent( - new CustomEvent('customSeek', { detail: -1, bubbles: true }) + new CustomEvent('seekCustomTodoLists', { detail: -1, bubbles: true }) ); }); el.querySelector('.forward').addEventListener('click', () => { el.dispatchEvent( - new CustomEvent('customSeek', { detail: 1, bubbles: true }) + new CustomEvent('seekCustomTodoLists', { detail: 1, bubbles: true }) ); }); @@ -50,7 +50,7 @@ export function TodoFrameCustom(el) { if (!e.detail.data.list) return; el.dispatchEvent( - new CustomEvent('moveList', { + new CustomEvent('moveTodoList', { detail: { list: e.detail.data.list, index: e.detail.index, diff --git a/public/scripts/TodoFrameDays.js b/public/scripts/TodoFrameDays.js index 3beccb1..4f6ecb3 100644 --- a/public/scripts/TodoFrameDays.js +++ b/public/scripts/TodoFrameDays.js @@ -1,3 +1,4 @@ +import { AppDatePicker } from './AppDatePicker.js'; import { AppIcon } from './AppIcon.js'; import { TodoDay } from './TodoDay.js'; import { formatDateId } from './util.js'; @@ -11,39 +12,78 @@ export function TodoFrameDays(el) { el.innerHTML = `
`; setTimeout(() => el.classList.add('-animated'), 200); el.querySelectorAll('.app-icon').forEach(AppIcon); + el.querySelectorAll('.app-date-picker').forEach(AppDatePicker); el.querySelector('.backward').addEventListener('click', () => - el.dispatchEvent(new CustomEvent('seek', { detail: -1, bubbles: true })) + el.dispatchEvent(new CustomEvent('seekDays', { detail: -1, bubbles: true })) ); el.querySelector('.forward').addEventListener('click', () => - el.dispatchEvent(new CustomEvent('seek', { detail: 1, bubbles: true })) + el.dispatchEvent(new CustomEvent('seekDays', { detail: 1, bubbles: true })) ); el.querySelector('.fastbackward').addEventListener('click', () => - el.dispatchEvent(new CustomEvent('seek', { detail: -5, bubbles: true })) + el.dispatchEvent(new CustomEvent('seekDays', { detail: -5, bubbles: true })) ); el.querySelector('.fastforward').addEventListener('click', () => - el.dispatchEvent(new CustomEvent('seek', { detail: 5, bubbles: true })) + el.dispatchEvent(new CustomEvent('seekDays', { detail: 5, bubbles: true })) ); el.querySelector('.home').addEventListener('click', () => - el.dispatchEvent(new CustomEvent('seekHome', { bubbles: true })) + el.dispatchEvent(new CustomEvent('seekToToday', { bubbles: true })) + ); + + el.querySelector('.pickdate').addEventListener('click', () => + el + .querySelector('.datepicker') + .dispatchEvent(new CustomEvent('toggleDatePicker')) + ); + + el.querySelector('.datepicker').addEventListener('pickDate', (e) => + el.dispatchEvent( + new CustomEvent('seekToDate', { detail: e.detail, bubbles: true }) + ) ); el.addEventListener('todoData', (e) => update(e.detail)); diff --git a/public/scripts/TodoItem.js b/public/scripts/TodoItem.js index 5d4edb6..46ca4b8 100644 --- a/public/scripts/TodoItem.js +++ b/public/scripts/TodoItem.js @@ -44,7 +44,7 @@ export function TodoItem(el) { if (state.editing) save(); el.dispatchEvent( - new CustomEvent('checkItem', { + new CustomEvent('checkTodoItem', { detail: { item: state.item, done: !state.item.done, @@ -102,7 +102,7 @@ export function TodoItem(el) { // event handler? requestAnimationFrame(() => { el.dispatchEvent( - new CustomEvent('deleteItem', { + new CustomEvent('deleteTodoItem', { detail: state.item, bubbles: true, }) @@ -113,7 +113,7 @@ export function TodoItem(el) { } el.dispatchEvent( - new CustomEvent('saveItem', { + new CustomEvent('saveTodoItem', { detail: { item: state.item, label, diff --git a/public/scripts/TodoItemInput.js b/public/scripts/TodoItemInput.js index 805dd81..1493238 100644 --- a/public/scripts/TodoItemInput.js +++ b/public/scripts/TodoItemInput.js @@ -48,7 +48,7 @@ export function TodoItemInput(el) { inputEl.value = ''; el.dispatchEvent( - new CustomEvent('addItem', { + new CustomEvent('addTodoItem', { detail: { label }, bubbles: true, }) diff --git a/public/scripts/TodoList.js b/public/scripts/TodoList.js index 3a3b648..aa7d416 100644 --- a/public/scripts/TodoList.js +++ b/public/scripts/TodoList.js @@ -17,7 +17,7 @@ export function TodoList(el) { el.addEventListener('sortableDrop', (e) => el.dispatchEvent( - new CustomEvent('moveItem', { + new CustomEvent('moveTodoItem', { detail: { item: e.detail.data.item, index: e.detail.index, diff --git a/public/scripts/TodoStore.js b/public/scripts/TodoStore.js index c9b3997..778ed7c 100644 --- a/public/scripts/TodoStore.js +++ b/public/scripts/TodoStore.js @@ -10,9 +10,9 @@ export function TodoStore(el) { let storeTimeout; - el.addEventListener('loadStore', load); + el.addEventListener('loadTodoStore', load); - el.addEventListener('addItem', (e) => { + el.addEventListener('addTodoItem', (e) => { let index = 0; for (const item of state.items) { @@ -32,21 +32,21 @@ export function TodoStore(el) { dispatch({ items: state.items }); }); - el.addEventListener('checkItem', (e) => { + el.addEventListener('checkTodoItem', (e) => { if (e.detail.item.done === e.detail.done) return; e.detail.item.done = e.detail.done; dispatch({ items: state.items }); }); - el.addEventListener('saveItem', (e) => { + el.addEventListener('saveTodoItem', (e) => { if (e.detail.item.label === e.detail.label) return; e.detail.item.label = e.detail.label; dispatch({ items: state.items }); }); - el.addEventListener('moveItem', (e) => { + el.addEventListener('moveTodoItem', (e) => { const movedItem = state.items.find((item) => item.id === e.detail.item.id); const listItems = state.items.filter( @@ -65,11 +65,11 @@ export function TodoStore(el) { dispatch({ items: state.items }); }); - el.addEventListener('deleteItem', (e) => { + el.addEventListener('deleteTodoItem', (e) => { dispatch({ items: state.items.filter((item) => item.id !== e.detail.id) }); }); - el.addEventListener('addList', (e) => { + el.addEventListener('addTodoList', (e) => { let index = 0; for (const customList of state.customLists) { @@ -85,7 +85,7 @@ export function TodoStore(el) { dispatch({ customLists: state.customLists }); }); - el.addEventListener('saveList', (e) => { + el.addEventListener('saveTodoList', (e) => { const list = state.customLists.find((l) => l.id === e.detail.list.id); if (list.title === e.detail.title) return; @@ -95,7 +95,7 @@ export function TodoStore(el) { dispatch({ customLists: state.customLists }); }); - el.addEventListener('moveList', (e) => { + el.addEventListener('moveTodoList', (e) => { const movedListIndex = state.customLists.findIndex( (list) => list.id === e.detail.list.id ); @@ -112,7 +112,7 @@ export function TodoStore(el) { dispatch({ customLists: state.customLists }); }); - el.addEventListener('deleteList', (e) => { + el.addEventListener('deleteTodoList', (e) => { dispatch({ customLists: state.customLists.filter( (customList) => customList.id !== e.detail.id @@ -120,18 +120,22 @@ export function TodoStore(el) { }); }); - el.addEventListener('seek', (e) => { + el.addEventListener('seekDays', (e) => { const t = new Date(`${state.at} 00:00:00`); t.setDate(t.getDate() + e.detail); dispatch({ at: formatDateId(t) }); }); - el.addEventListener('seekHome', () => + el.addEventListener('seekToToday', () => dispatch({ at: formatDateId(new Date()) }) ); - el.addEventListener('customSeek', (e) => { + el.addEventListener('seekToDate', (e) => + dispatch({ at: formatDateId(e.detail) }) + ); + + el.addEventListener('seekCustomTodoLists', (e) => { dispatch({ customAt: Math.max( 0, diff --git a/public/styles/app-button.css b/public/styles/app-button.css index 74fa0a1..39e7914 100644 --- a/public/styles/app-button.css +++ b/public/styles/app-button.css @@ -38,6 +38,15 @@ font-size: 1.5em; } +.app-button.-highlight { + background: #111; + color: #fff; +} + +.app-button.-highlight:hover { + color: #eee; +} + @media (min-width: 600px) { .app-button.-xl { font-size: 2em; diff --git a/public/styles/app-date-picker.css b/public/styles/app-date-picker.css new file mode 100644 index 0000000..eefe4a1 --- /dev/null +++ b/public/styles/app-date-picker.css @@ -0,0 +1,51 @@ +.app-date-picker { + width: 260px; + background: #fff; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 10%) 0 4px 12px; + padding: 8px; + transform: translate(110%, 0); + transition: all 0.2s ease-in-out; + text-align: center; +} + +.app-date-picker.-show { + transform: translate(0, 0); +} + +.app-date-picker > .header { + display: flex; + font-size: 1em; + margin: 0 0 1em 0; + line-height: 1.5em; +} + +.app-date-picker > .header > .month { + flex-grow: 1; + font-weight: bold; + text-transform: uppercase; +} + +.app-date-picker > .dates { + width: 100%; +} + +.app-date-picker > .dates > thead > tr > th { + font-weight: normal; + padding: 0; +} + +.app-date-picker > .dates > tbody > tr > td { + padding: 0; +} + +.app-date-picker > .dates > tbody > tr > td > button { + width: 100%; + height: 1.9em; +} + +@media (min-width: 320px) { + .app-date-picker { + width: 300px; + } +} diff --git a/public/styles/todo-app.css b/public/styles/todo-app.css new file mode 100644 index 0000000..c7454c6 --- /dev/null +++ b/public/styles/todo-app.css @@ -0,0 +1,3 @@ +.todo-app { + overflow: hidden; +} diff --git a/public/styles/todo-frame.css b/public/styles/todo-frame.css index 29d2111..34d293b 100644 --- a/public/styles/todo-frame.css +++ b/public/styles/todo-frame.css @@ -1,6 +1,5 @@ .todo-frame { position: relative; - overflow: hidden; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -31,6 +30,11 @@ margin: 0 0 0.5em 0; } +.todo-frame > .rightcontrols > .datepicker { + position: absolute; + right: 10px; +} + .todo-frame > .container { position: absolute; overflow: hidden;
SuMoTuWeThFrSa