1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-08-21 05:11:20 +02:00

refactor state

This commit is contained in:
Morris Brodersen
2023-11-26 11:54:04 +01:00
parent 9343da1693
commit 6a640515b2
12 changed files with 209 additions and 203 deletions

View File

@@ -231,11 +231,9 @@ Here's a "Hello, World!" example of mount functions:
// Loosely mapped to ".hello-world" // Loosely mapped to ".hello-world"
export function HelloWorld(el) { export function HelloWorld(el) {
// Define initial state // Define initial state
const state = { let title = 'Hello, World!';
title: 'Hello, World!', let description = 'An example vanilla component';
description: 'An example vanilla component', let counter = 0;
counter: 0,
};
// Set rigid base HTML // Set rigid base HTML
el.innerHTML = ` el.innerHTML = `
@@ -248,27 +246,24 @@ export function HelloWorld(el) {
el.querySelectorAll('.my-counter').forEach(MyCounter); el.querySelectorAll('.my-counter').forEach(MyCounter);
// Attach event listeners // Attach event listeners
el.addEventListener('modifyCounter', (e) => el.addEventListener('modifyCounter', (e) => {
update({ counter: state.counter + e.detail }), counter += e.detail;
); update();
});
// Initial update // Initial update
update(); update();
// Define idempotent update function // Define idempotent update function
function update(next) { function update() {
// Update state
// Optionally optimize, e.g. bail out if state hasn't changed
Object.assign(state, next);
// Update own HTML // Update own HTML
el.querySelector('.title').innerText = state.title; el.querySelector('.title').innerText = title;
el.querySelector('.description').innerText = state.description; el.querySelector('.description').innerText = description;
// Pass data to sub-scomponents // Pass data to sub-components
el.querySelector('.my-counter').dispatchEvent( el.querySelector('.my-counter').dispatchEvent(
new CustomEvent('updateMyCounter', { new CustomEvent('updateMyCounter', {
detail: { value: state.counter }, detail: { value: counter },
}), }),
); );
} }
@@ -278,9 +273,7 @@ export function HelloWorld(el) {
// Loosely mapped to ".my-counter" // Loosely mapped to ".my-counter"
export function MyCounter(el) { export function MyCounter(el) {
// Define initial state // Define initial state
const state = { let value = 0;
value: 0,
};
// Set rigid base HTML // Set rigid base HTML
el.innerHTML = ` el.innerHTML = `
@@ -314,13 +307,14 @@ export function MyCounter(el) {
); );
}); });
el.addEventListener('updateMyCounter', (e) => update(e.detail)); el.addEventListener('updateMyCounter', (e) => {
value = e.detail;
update();
});
// Define idempotent update function // Define idempotent update function
function update(next) { function update() {
Object.assign(state, next); el.querySelector('.value').innerText = value;
el.querySelector('.value').innerText = state.value;
} }
} }
@@ -416,17 +410,16 @@ from the implementation outlining the reconciliation algorithm:
```js ```js
export function TodoList(el) { export function TodoList(el) {
const state = { let items = [];
items: [],
};
el.innerHTML = `<div class="items"></div>`; el.innerHTML = `<div class="items"></div>`;
el.addEventListener('updateTodoList', (e) => update(e.detail)); el.addEventListener('updateTodoList', (e) => {
items = e.detail;
function update(next) { update();
Object.assign(state, next); });
function update() {
const container = el.querySelector('.items'); const container = el.querySelector('.items');
// Mark current children for removal // Mark current children for removal
@@ -440,7 +433,7 @@ export function TodoList(el) {
); );
// Build new list of child elements from data // Build new list of child elements from data
const children = state.items.map((item) => { const children = items.map((item) => {
// Find existing child by data-key // Find existing child by data-key
let child = childrenByKey.get(item.id); let child = childrenByKey.get(item.id);

View File

@@ -2,32 +2,33 @@
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function AppCollapsible(el) { export function AppCollapsible(el) {
const state = { let show = true;
show: true,
};
setTimeout(() => el.classList.add('-animated'), 200); setTimeout(() => el.classList.add('-animated'), 200);
el.addEventListener('collapse', (e) => { el.addEventListener('collapse', (e) => {
update({ show: typeof e.detail === 'boolean' ? !e.detail : state.show }); if (typeof e.detail === 'boolean') {
show = e.detail;
}
update();
}); });
el.querySelector('.bar > .toggle').addEventListener('click', () => { el.querySelector('.bar > .toggle').addEventListener('click', () => {
update({ show: !state.show }); show = !show;
update();
}); });
update(); update();
function update(next) { function update() {
Object.assign(state, next);
el.querySelector('.bar > .toggle > .app-icon').classList.toggle( el.querySelector('.bar > .toggle > .app-icon').classList.toggle(
'-r180', '-r180',
state.show, show,
); );
el.querySelectorAll('.body').forEach((el) => { el.querySelectorAll('.body').forEach((el) => {
el.style.height = state.show ? `${el.children[0].offsetHeight}px` : '0'; el.style.height = show ? `${el.children[0].offsetHeight}px` : '0';
}); });
} }
} }

View File

@@ -22,11 +22,11 @@ const datesRow = `
*/ */
export function AppDatePicker(el) { export function AppDatePicker(el) {
const now = new Date(); const now = new Date();
const state = { let at = {
year: now.getFullYear(), year: now.getFullYear(),
month: now.getMonth() + 1, month: now.getMonth() + 1,
show: false,
}; };
let show = false;
el.innerHTML = ` el.innerHTML = `
<h4 class="header"> <h4 class="header">
@@ -62,11 +62,10 @@ export function AppDatePicker(el) {
el.querySelectorAll('.app-icon').forEach(AppIcon); el.querySelectorAll('.app-icon').forEach(AppIcon);
el.addEventListener('toggleDatePicker', (e) => el.addEventListener('toggleDatePicker', (e) => {
update({ show: e.detail ?? !state.show }), show = e.detail ?? !show;
); update();
});
el.addEventListener('setMonth', (e) => update(e.detail));
el.querySelector('.previousmonth').addEventListener('click', previousMonth); el.querySelector('.previousmonth').addEventListener('click', previousMonth);
el.querySelector('.nextmonth').addEventListener('click', nextMonth); el.querySelector('.nextmonth').addEventListener('click', nextMonth);
@@ -74,7 +73,8 @@ export function AppDatePicker(el) {
el.addEventListener('click', (e) => { el.addEventListener('click', (e) => {
if (!e.target.matches('.app-button')) return; if (!e.target.matches('.app-button')) return;
update({ show: false }); show = false;
update();
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('pickDate', { new CustomEvent('pickDate', {
@@ -89,40 +89,42 @@ export function AppDatePicker(el) {
}); });
function previousMonth() { function previousMonth() {
update( if (at.month > 1) {
state.month > 1 at = {
? { year: at.year,
year: state.year, month: at.month - 1,
month: state.month - 1, };
} } else {
: { at = {
year: state.year - 1, year: at.year - 1,
month: 12, month: 12,
}, };
); }
update();
} }
function nextMonth() { function nextMonth() {
update( if (at.month < 12) {
state.month < 12 at = {
? { year: at.year,
year: state.year, month: at.month + 1,
month: state.month + 1, };
} } else {
: { at = {
year: state.year + 1, year: at.year + 1,
month: 1, month: 1,
}, };
); }
update();
} }
function update(next) { function update() {
Object.assign(state, next); el.classList.toggle('-show', show);
el.classList.toggle('-show', state.show);
const now = new Date(); const now = new Date();
const first = new Date(state.year, state.month - 1, 1); const first = new Date(at.year, at.month - 1, 1);
el.querySelector('.month').innerHTML = `${formatMonth( el.querySelector('.month').innerHTML = `${formatMonth(
first, first,

View File

@@ -7,9 +7,10 @@
export function AppSortable(el, options) { export function AppSortable(el, options) {
let placeholder; let placeholder;
let placeholderSource; let placeholderSource;
const horizontal = options.direction === 'horizontal';
let currentIndex = -1; let currentIndex = -1;
const isBefore = options.direction === 'horizontal' ? isLeft : isAbove;
el.addEventListener('draggableStart', (e) => el.addEventListener('draggableStart', (e) =>
e.detail.image.addEventListener('draggableCancel', cleanUp), e.detail.image.addEventListener('draggableCancel', cleanUp),
); );
@@ -115,7 +116,6 @@ export function AppSortable(el, options) {
function calculateIndex(image) { function calculateIndex(image) {
if (el.children.length === 0) return 0; if (el.children.length === 0) return 0;
const isBefore = horizontal ? isLeft : isAbove;
const rect = image.getBoundingClientRect(); const rect = image.getBoundingClientRect();
let p = 0; let p = 0;

View File

@@ -11,7 +11,7 @@ import { formatDateId } from './util.js';
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function TodoApp(el) { export function TodoApp(el) {
const state = { let todoData = {
items: [], items: [],
customLists: [], customLists: [],
at: formatDateId(new Date()), at: formatDateId(new Date()),
@@ -83,7 +83,7 @@ export function TodoApp(el) {
e.detail.placeholder.classList.add('_noflip'); e.detail.placeholder.classList.add('_noflip');
}); });
// dispatch "focusOther" on .use-focus-other inputs if they are not active // dispatch "focusOther" on .use-focus-other inputs if they are not active
// ensures only one edit input is active // ensures only one edit input is active
el.addEventListener('focusin', (e) => { el.addEventListener('focusin', (e) => {
if (!e.target.classList.contains('use-focus-other')) return; if (!e.target.classList.contains('use-focus-other')) return;
@@ -97,7 +97,10 @@ export function TodoApp(el) {
// listen to the TodoStore's data // listen to the TodoStore's data
// this is the main update // this is the main update
// everything else is related to drag & drop or FLIP animations // everything else is related to drag & drop or FLIP animations
el.addEventListener('todoData', (e) => update(e.detail)); el.addEventListener('todoData', (e) => {
todoData = e.detail;
update();
});
// dispatch "flip" after HTML changes from these events // dispatch "flip" after HTML changes from these events
// this plays the FLIP animations // this plays the FLIP animations
@@ -108,11 +111,9 @@ export function TodoApp(el) {
el.dispatchEvent(new CustomEvent('loadTodoStore')); el.dispatchEvent(new CustomEvent('loadTodoStore'));
function update(next) { function update() {
Object.assign(state, next);
el.querySelectorAll('.todo-frame').forEach((el) => el.querySelectorAll('.todo-frame').forEach((el) =>
el.dispatchEvent(new CustomEvent('todoData', { detail: state })), el.dispatchEvent(new CustomEvent('todoData', { detail: todoData })),
); );
el.querySelectorAll('.app-collapsible').forEach((el) => el.querySelectorAll('.app-collapsible').forEach((el) =>

View File

@@ -6,10 +6,8 @@ import { TodoList } from './TodoList.js';
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function TodoCustomList(el) { export function TodoCustomList(el) {
const state = { let list;
list: null, let editing = false;
editing: false,
};
let startEditing = false; let startEditing = false;
let saveOnBlur = true; let saveOnBlur = true;
@@ -36,7 +34,8 @@ export function TodoCustomList(el) {
titleEl.addEventListener('click', () => { titleEl.addEventListener('click', () => {
startEditing = true; startEditing = true;
update({ editing: true }); editing = true;
update();
}); });
deleteEl.addEventListener('touchstart', () => { deleteEl.addEventListener('touchstart', () => {
@@ -53,7 +52,7 @@ export function TodoCustomList(el) {
}); });
inputEl.addEventListener('focusOther', () => { inputEl.addEventListener('focusOther', () => {
if (state.editing) save(); if (editing) save();
}); });
inputEl.addEventListener('keyup', (e) => { inputEl.addEventListener('keyup', (e) => {
@@ -68,7 +67,7 @@ export function TodoCustomList(el) {
}); });
deleteEl.addEventListener('click', () => { deleteEl.addEventListener('click', () => {
if (state.list.items.length > 0) { if (list.items.length > 0) {
if ( if (
!confirm( !confirm(
'Deleting this list will delete its items as well. Are you sure?', 'Deleting this list will delete its items as well. Are you sure?',
@@ -80,7 +79,7 @@ export function TodoCustomList(el) {
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('deleteTodoList', { new CustomEvent('deleteTodoList', {
detail: state.list, detail: list,
bubbles: true, bubbles: true,
}), }),
); );
@@ -89,8 +88,8 @@ export function TodoCustomList(el) {
el.addEventListener('draggableStart', (e) => { el.addEventListener('draggableStart', (e) => {
if (e.target !== titleEl) return; if (e.target !== titleEl) return;
e.detail.data.list = state.list; e.detail.data.list = list;
e.detail.data.key = state.list.id; e.detail.data.key = list.id;
// update image (default would only be title element) // update image (default would only be title element)
e.detail.setImage(el); e.detail.setImage(el);
@@ -104,47 +103,50 @@ export function TodoCustomList(el) {
}); });
el.addEventListener('addTodoItem', (e) => { el.addEventListener('addTodoItem', (e) => {
e.detail.listId = state.list.id; e.detail.listId = list.id;
}); });
el.addEventListener('moveTodoItem', (e) => { el.addEventListener('moveTodoItem', (e) => {
e.detail.listId = state.list.id; e.detail.listId = list.id;
e.detail.index = e.detail.index ?? 0; e.detail.index = e.detail.index ?? 0;
}); });
el.addEventListener('todoCustomList', (e) => update({ list: e.detail })); el.addEventListener('todoCustomList', (e) => {
list = e.detail;
update();
});
function save() { function save() {
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('saveTodoList', { new CustomEvent('saveTodoList', {
detail: { list: state.list, title: inputEl.value.trim() }, detail: { list, title: inputEl.value.trim() },
bubbles: true, bubbles: true,
}), }),
); );
update({ editing: false }); editing = false;
update();
} }
function cancelEdit() { function cancelEdit() {
saveOnBlur = false; saveOnBlur = false;
update({ editing: false }); editing = false;
update();
} }
function update(next) { function update() {
Object.assign(state, next); titleEl.innerText = list.title || '...';
titleEl.innerText = state.list.title || '...';
el.querySelector('.todo-list').dispatchEvent( el.querySelector('.todo-list').dispatchEvent(
new CustomEvent('todoItems', { detail: state.list.items }), new CustomEvent('todoItems', { detail: list.items }),
); );
el.querySelector('.todo-list > .todo-item-input').dataset.key = el.querySelector('.todo-list > .todo-item-input').dataset.key =
`todo-item-input${state.list.id}`; `todo-item-input${list.id}`;
el.classList.toggle('-editing', state.editing); el.classList.toggle('-editing', editing);
if (state.editing && startEditing) { if (editing && startEditing) {
inputEl.value = state.list.title; inputEl.value = list.title;
inputEl.focus(); inputEl.focus();
inputEl.select(); inputEl.select();
startEditing = false; startEditing = false;

View File

@@ -5,10 +5,8 @@ import { formatDate, formatDayOfWeek } from './util.js';
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function TodoDay(el) { export function TodoDay(el) {
const state = { const dateId = el.dataset.key;
dateId: el.dataset.key, let items = [];
items: [],
};
el.innerHTML = ` el.innerHTML = `
<div class="header"> <div class="header">
@@ -21,20 +19,21 @@ export function TodoDay(el) {
TodoList(el.querySelector('.todo-list')); TodoList(el.querySelector('.todo-list'));
el.addEventListener('addTodoItem', (e) => { el.addEventListener('addTodoItem', (e) => {
e.detail.listId = state.dateId; e.detail.listId = dateId;
}); });
el.addEventListener('moveTodoItem', (e) => { el.addEventListener('moveTodoItem', (e) => {
e.detail.listId = state.dateId; e.detail.listId = dateId;
e.detail.index = e.detail.index ?? 0; e.detail.index = e.detail.index ?? 0;
}); });
el.addEventListener('todoDay', (e) => update(e.detail)); el.addEventListener('todoDay', (e) => {
items = e.detail.items;
update();
});
function update(next) { function update() {
Object.assign(state, next); const date = new Date(dateId);
const date = new Date(state.dateId);
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today); const tomorrow = new Date(today);
@@ -46,7 +45,7 @@ export function TodoDay(el) {
el.querySelector('.header > .dayofweek').innerText = formatDayOfWeek(date); el.querySelector('.header > .dayofweek').innerText = formatDayOfWeek(date);
el.querySelector('.header > .date').innerText = formatDate(date); el.querySelector('.header > .date').innerText = formatDate(date);
el.querySelector('.todo-list').dispatchEvent( el.querySelector('.todo-list').dispatchEvent(
new CustomEvent('todoItems', { detail: state.items }), new CustomEvent('todoItems', { detail: items }),
); );
} }
} }

View File

@@ -6,11 +6,10 @@ import { TodoCustomList } from './TodoCustomList.js';
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function TodoFrameCustom(el) { export function TodoFrameCustom(el) {
const state = { let todoData = {
customLists: [], customLists: [],
items: [], items: [],
customAt: 0, customAt: 0,
show: true,
}; };
el.innerHTML = ` el.innerHTML = `
@@ -69,11 +68,12 @@ export function TodoFrameCustom(el) {
updatePositions(); updatePositions();
}); });
el.addEventListener('todoData', (e) => update(e.detail)); el.addEventListener('todoData', (e) => {
todoData = e.detail;
function update(next) { update();
Object.assign(state, next); });
function update() {
const lists = getLists(); const lists = getLists();
const container = el.querySelector('.container'); const container = el.querySelector('.container');
const obsolete = new Set(container.children); const obsolete = new Set(container.children);
@@ -112,7 +112,9 @@ export function TodoFrameCustom(el) {
function updatePositions() { function updatePositions() {
el.querySelectorAll('.container > *').forEach((child, index) => { el.querySelectorAll('.container > *').forEach((child, index) => {
child.style.transform = `translateX(${(index - state.customAt) * 100}%)`; child.style.transform = `translateX(${
(index - todoData.customAt) * 100
}%)`;
}); });
} }
@@ -136,7 +138,7 @@ export function TodoFrameCustom(el) {
} }
function getLists() { function getLists() {
return state.customLists return todoData.customLists
.map((list) => ({ .map((list) => ({
id: list.id, id: list.id,
index: list.index, index: list.index,
@@ -147,7 +149,7 @@ export function TodoFrameCustom(el) {
} }
function getItemsForList(listId) { function getItemsForList(listId) {
return state.items return todoData.items
.filter((item) => item.listId === listId) .filter((item) => item.listId === listId)
.sort((a, b) => a.index - b.index); .sort((a, b) => a.index - b.index);
} }

View File

@@ -8,7 +8,7 @@ import { formatDateId } from './util.js';
*/ */
export function TodoFrameDays(el) { export function TodoFrameDays(el) {
const RANGE = 14; const RANGE = 14;
const state = { let todoData = {
items: [], items: [],
at: formatDateId(new Date()), at: formatDateId(new Date()),
}; };
@@ -93,11 +93,12 @@ export function TodoFrameDays(el) {
), ),
); );
el.addEventListener('todoData', (e) => update(e.detail)); el.addEventListener('todoData', (e) => {
todoData = e.detail;
function update(next) { update();
Object.assign(state, next); });
function update() {
const days = getDays(); const days = getDays();
const container = el.querySelector('.container'); const container = el.querySelector('.container');
@@ -150,7 +151,7 @@ export function TodoFrameDays(el) {
const days = []; const days = [];
for (let i = 0; i < 2 * RANGE; ++i) { for (let i = 0; i < 2 * RANGE; ++i) {
const t = new Date(state.at); const t = new Date(todoData.at);
t.setDate(t.getDate() - RANGE + i); t.setDate(t.getDate() - RANGE + i);
const id = formatDateId(t); const id = formatDateId(t);
@@ -165,7 +166,7 @@ export function TodoFrameDays(el) {
} }
function getItemsForDay(dateId) { function getItemsForDay(dateId) {
return state.items return todoData.items
.filter((item) => item.listId === dateId) .filter((item) => item.listId === dateId)
.sort((a, b) => a.index - b.index); .sort((a, b) => a.index - b.index);
} }

View File

@@ -5,11 +5,8 @@ import { AppIcon } from './AppIcon.js';
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function TodoItem(el) { export function TodoItem(el) {
const state = { let item;
item: null, let editing = false;
editing: false,
};
let startEditing = false; let startEditing = false;
let saveOnBlur = true; let saveOnBlur = true;
@@ -44,13 +41,13 @@ export function TodoItem(el) {
}); });
checkboxEl.addEventListener('click', () => { checkboxEl.addEventListener('click', () => {
if (state.editing) save(); if (editing) save();
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('checkTodoItem', { new CustomEvent('checkTodoItem', {
detail: { detail: {
item: state.item, item,
done: !state.item.done, done: !item.done,
}, },
bubbles: true, bubbles: true,
}), }),
@@ -59,7 +56,8 @@ export function TodoItem(el) {
labelEl.addEventListener('click', () => { labelEl.addEventListener('click', () => {
startEditing = true; startEditing = true;
update({ editing: true }); editing = true;
update();
}); });
inputEl.addEventListener('keyup', (e) => { inputEl.addEventListener('keyup', (e) => {
@@ -79,7 +77,7 @@ export function TodoItem(el) {
}); });
inputEl.addEventListener('focusOther', () => { inputEl.addEventListener('focusOther', () => {
if (state.editing) save(); if (editing) save();
}); });
saveEl.addEventListener('mousedown', () => { saveEl.addEventListener('mousedown', () => {
@@ -89,11 +87,14 @@ export function TodoItem(el) {
saveEl.addEventListener('click', save); saveEl.addEventListener('click', save);
el.addEventListener('draggableStart', (e) => { el.addEventListener('draggableStart', (e) => {
e.detail.data.item = state.item; e.detail.data.item = item;
e.detail.data.key = state.item.id; e.detail.data.key = item.id;
}); });
el.addEventListener('todoItem', (e) => update({ item: e.detail })); el.addEventListener('todoItem', (e) => {
item = e.detail;
update();
});
function save() { function save() {
const label = inputEl.value.trim(); const label = inputEl.value.trim();
@@ -106,7 +107,7 @@ export function TodoItem(el) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('deleteTodoItem', { new CustomEvent('deleteTodoItem', {
detail: state.item, detail: item,
bubbles: true, bubbles: true,
}), }),
); );
@@ -118,34 +119,35 @@ export function TodoItem(el) {
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('saveTodoItem', { new CustomEvent('saveTodoItem', {
detail: { detail: {
item: state.item, item,
label, label,
}, },
bubbles: true, bubbles: true,
}), }),
); );
update({ editing: false }); editing = false;
update();
} }
function cancelEdit() { function cancelEdit() {
saveOnBlur = false; saveOnBlur = false;
update({ editing: false }); editing = false;
update();
} }
function update(next) { function update() {
// TODO optimize // TODO optimize
Object.assign(state, next);
el.classList.toggle('-done', state.item.done); el.classList.toggle('-done', item.done);
checkboxEl.querySelector('input').checked = state.item.done; checkboxEl.querySelector('input').checked = item.done;
labelEl.innerText = state.item.label; labelEl.innerText = item.label;
el.classList.toggle('-editing', state.editing); el.classList.toggle('-editing', editing);
el.classList.toggle('_nodrag', state.editing); el.classList.toggle('_nodrag', editing);
if (state.editing && startEditing) { if (editing && startEditing) {
inputEl.value = state.item.label; inputEl.value = item.label;
inputEl.focus(); inputEl.focus();
inputEl.select(); inputEl.select();
startEditing = false; startEditing = false;

View File

@@ -6,9 +6,7 @@ import { TodoItemInput } from './TodoItemInput.js';
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function TodoList(el) { export function TodoList(el) {
const state = { let items = [];
items: [],
};
el.innerHTML = ` el.innerHTML = `
<div class="items"></div> <div class="items"></div>
@@ -30,18 +28,19 @@ export function TodoList(el) {
), ),
); );
el.addEventListener('todoItems', (e) => update({ items: e.detail })); el.addEventListener('todoItems', (e) => {
items = e.detail;
function update(next) { update();
Object.assign(state, next); });
function update() {
const container = el.querySelector('.items'); const container = el.querySelector('.items');
const obsolete = new Set(container.children); const obsolete = new Set(container.children);
const childrenByKey = new Map(); const childrenByKey = new Map();
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child)); obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
const children = state.items.map((item) => { const children = items.map((item) => {
let child = childrenByKey.get(item.id); let child = childrenByKey.get(item.id);
if (child) { if (child) {

View File

@@ -4,7 +4,7 @@ import { formatDateId, uuid } from './util.js';
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
export function TodoStore(el) { export function TodoStore(el) {
const state = { const todoData = {
items: [], items: [],
customLists: [], customLists: [],
at: formatDateId(new Date()), at: formatDateId(new Date()),
@@ -18,13 +18,13 @@ export function TodoStore(el) {
el.addEventListener('addTodoItem', (e) => { el.addEventListener('addTodoItem', (e) => {
let index = 0; let index = 0;
for (const item of state.items) { for (const item of todoData.items) {
if (item.listId === e.detail.listId) { if (item.listId === e.detail.listId) {
index = Math.max(index, item.index + 1); index = Math.max(index, item.index + 1);
} }
} }
state.items.push({ todoData.items.push({
id: uuid(), id: uuid(),
listId: e.detail.listId, listId: e.detail.listId,
index, index,
@@ -32,27 +32,29 @@ export function TodoStore(el) {
done: false, done: false,
}); });
dispatch({ items: state.items }); dispatch({ items: todoData.items });
}); });
el.addEventListener('checkTodoItem', (e) => { el.addEventListener('checkTodoItem', (e) => {
if (e.detail.item.done === e.detail.done) return; if (e.detail.item.done === e.detail.done) return;
e.detail.item.done = e.detail.done; e.detail.item.done = e.detail.done;
dispatch({ items: state.items }); dispatch({ items: todoData.items });
}); });
el.addEventListener('saveTodoItem', (e) => { el.addEventListener('saveTodoItem', (e) => {
if (e.detail.item.label === e.detail.label) return; if (e.detail.item.label === e.detail.label) return;
e.detail.item.label = e.detail.label; e.detail.item.label = e.detail.label;
dispatch({ items: state.items }); dispatch({ items: todoData.items });
}); });
el.addEventListener('moveTodoItem', (e) => { el.addEventListener('moveTodoItem', (e) => {
const movedItem = state.items.find((item) => item.id === e.detail.item.id); const movedItem = todoData.items.find(
(item) => item.id === e.detail.item.id,
);
const listItems = state.items.filter( const listItems = todoData.items.filter(
(item) => item.listId === e.detail.listId && item !== movedItem, (item) => item.listId === e.detail.listId && item !== movedItem,
); );
@@ -65,66 +67,68 @@ export function TodoStore(el) {
item.index = index; item.index = index;
}); });
dispatch({ items: state.items }); dispatch({ items: todoData.items });
}); });
el.addEventListener('deleteTodoItem', (e) => el.addEventListener('deleteTodoItem', (e) =>
dispatch({ items: state.items.filter((item) => item.id !== e.detail.id) }), dispatch({
items: todoData.items.filter((item) => item.id !== e.detail.id),
}),
); );
el.addEventListener('addTodoList', (e) => { el.addEventListener('addTodoList', (e) => {
let index = 0; let index = 0;
for (const customList of state.customLists) { for (const customList of todoData.customLists) {
index = Math.max(index, customList.index + 1); index = Math.max(index, customList.index + 1);
} }
state.customLists.push({ todoData.customLists.push({
id: uuid(), id: uuid(),
index, index,
title: e.detail.title || '', title: e.detail.title || '',
}); });
dispatch({ customLists: state.customLists }); dispatch({ customLists: todoData.customLists });
}); });
el.addEventListener('saveTodoList', (e) => { el.addEventListener('saveTodoList', (e) => {
const list = state.customLists.find((l) => l.id === e.detail.list.id); const list = todoData.customLists.find((l) => l.id === e.detail.list.id);
if (list.title === e.detail.title) return; if (list.title === e.detail.title) return;
list.title = e.detail.title; list.title = e.detail.title;
dispatch({ customLists: state.customLists }); dispatch({ customLists: todoData.customLists });
}); });
el.addEventListener('moveTodoList', (e) => { el.addEventListener('moveTodoList', (e) => {
const movedListIndex = state.customLists.findIndex( const movedListIndex = todoData.customLists.findIndex(
(list) => list.id === e.detail.list.id, (list) => list.id === e.detail.list.id,
); );
const movedList = state.customLists[movedListIndex]; const movedList = todoData.customLists[movedListIndex];
state.customLists.splice(movedListIndex, 1); todoData.customLists.splice(movedListIndex, 1);
state.customLists.sort((a, b) => a.index - b.index); todoData.customLists.sort((a, b) => a.index - b.index);
state.customLists.splice(e.detail.index, 0, movedList); todoData.customLists.splice(e.detail.index, 0, movedList);
state.customLists.forEach((item, index) => { todoData.customLists.forEach((item, index) => {
item.index = index; item.index = index;
}); });
dispatch({ customLists: state.customLists }); dispatch({ customLists: todoData.customLists });
}); });
el.addEventListener('deleteTodoList', (e) => el.addEventListener('deleteTodoList', (e) =>
dispatch({ dispatch({
customLists: state.customLists.filter( customLists: todoData.customLists.filter(
(customList) => customList.id !== e.detail.id, (customList) => customList.id !== e.detail.id,
), ),
}), }),
); );
el.addEventListener('seekDays', (e) => { el.addEventListener('seekDays', (e) => {
const t = new Date(`${state.at}T00:00:00`); const t = new Date(`${todoData.at}T00:00:00`);
t.setDate(t.getDate() + e.detail); t.setDate(t.getDate() + e.detail);
dispatch({ at: formatDateId(t) }); dispatch({ at: formatDateId(t) });
@@ -142,18 +146,18 @@ export function TodoStore(el) {
dispatch({ dispatch({
customAt: Math.max( customAt: Math.max(
0, 0,
Math.min(state.customLists.length - 1, state.customAt + e.detail), Math.min(todoData.customLists.length - 1, todoData.customAt + e.detail),
), ),
}), }),
); );
function dispatch(next) { function dispatch(next) {
Object.assign(state, next); Object.assign(todoData, next);
save(); save();
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('todoData', { new CustomEvent('todoData', {
detail: state, detail: todoData,
bubbles: false, bubbles: false,
}), }),
); );
@@ -161,7 +165,7 @@ export function TodoStore(el) {
function load() { function load() {
if (!localStorage || !localStorage.todo) { if (!localStorage || !localStorage.todo) {
dispatch(state); dispatch(todoData);
return; return;
} }
@@ -178,7 +182,7 @@ export function TodoStore(el) {
storeTimeout = setTimeout(() => { storeTimeout = setTimeout(() => {
try { try {
localStorage.todo = JSON.stringify(state); localStorage.todo = JSON.stringify(todoData);
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn(err); console.warn(err);