mirror of
https://github.com/morris/vanilla-todo.git
synced 2025-08-21 05:11:20 +02:00
refactor for pure functional business logic
This commit is contained in:
@@ -4,19 +4,14 @@ import { AppFps } from './AppFps.js';
|
|||||||
import { AppIcon } from './AppIcon.js';
|
import { AppIcon } from './AppIcon.js';
|
||||||
import { TodoFrameCustom } from './TodoFrameCustom.js';
|
import { TodoFrameCustom } from './TodoFrameCustom.js';
|
||||||
import { TodoFrameDays } from './TodoFrameDays.js';
|
import { TodoFrameDays } from './TodoFrameDays.js';
|
||||||
|
import { TodoLogic } from './TodoLogic.js';
|
||||||
import { TodoStore } from './TodoStore.js';
|
import { TodoStore } from './TodoStore.js';
|
||||||
import { formatDateId } from './util.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} el
|
* @param {HTMLElement} el
|
||||||
*/
|
*/
|
||||||
export function TodoApp(el) {
|
export function TodoApp(el) {
|
||||||
let todoData = {
|
let todoData = TodoLogic.initTodoData();
|
||||||
items: [],
|
|
||||||
customLists: [],
|
|
||||||
at: formatDateId(new Date()),
|
|
||||||
customAt: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
|
@@ -82,7 +82,7 @@ export function TodoCustomList(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('deleteTodoList', {
|
new CustomEvent('deleteCustomTodoList', {
|
||||||
detail: list,
|
detail: list,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
}),
|
}),
|
||||||
@@ -115,15 +115,15 @@ export function TodoCustomList(el) {
|
|||||||
e.detail.index = e.detail.index ?? 0;
|
e.detail.index = e.detail.index ?? 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('todoCustomList', (e) => {
|
el.addEventListener('customTodoList', (e) => {
|
||||||
list = e.detail;
|
list = e.detail;
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('saveTodoList', {
|
new CustomEvent('editCustomTodoList', {
|
||||||
detail: { list, title: inputEl.value.trim() },
|
detail: { ...list, title: inputEl.value.trim() },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
import { AppIcon } from './AppIcon.js';
|
import { AppIcon } from './AppIcon.js';
|
||||||
import { AppSortable } from './AppSortable.js';
|
import { AppSortable } from './AppSortable.js';
|
||||||
import { TodoCustomList } from './TodoCustomList.js';
|
import { TodoCustomList } from './TodoCustomList.js';
|
||||||
|
import { TodoLogic } from './TodoLogic.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} el
|
* @param {HTMLElement} el
|
||||||
*/
|
*/
|
||||||
export function TodoFrameCustom(el) {
|
export function TodoFrameCustom(el) {
|
||||||
let todoData = {
|
let todoData = TodoLogic.initTodoData();
|
||||||
customLists: [],
|
|
||||||
items: [],
|
|
||||||
customAt: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="leftcontrols">
|
<div class="leftcontrols">
|
||||||
@@ -42,9 +39,7 @@ export function TodoFrameCustom(el) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
el.querySelector('.add').addEventListener('click', () => {
|
el.querySelector('.add').addEventListener('click', () => {
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(new CustomEvent('addCustomTodoList', { bubbles: true }));
|
||||||
new CustomEvent('addTodoList', { detail: {}, bubbles: true }),
|
|
||||||
);
|
|
||||||
// TODO seek if not at end
|
// TODO seek if not at end
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,9 +47,9 @@ export function TodoFrameCustom(el) {
|
|||||||
if (!e.detail.data.list) return;
|
if (!e.detail.data.list) return;
|
||||||
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('moveTodoList', {
|
new CustomEvent('moveCustomTodoList', {
|
||||||
detail: {
|
detail: {
|
||||||
list: e.detail.data.list,
|
...e.detail.data.list,
|
||||||
index: e.detail.index,
|
index: e.detail.index,
|
||||||
},
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -63,9 +58,7 @@ export function TodoFrameCustom(el) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('draggableOver', (e) => {
|
el.addEventListener('draggableOver', (e) => {
|
||||||
if (!e.detail.data.list) return;
|
if (e.detail.data.list) updatePositions();
|
||||||
|
|
||||||
updatePositions();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('todoData', (e) => {
|
el.addEventListener('todoData', (e) => {
|
||||||
@@ -74,14 +67,15 @@ export function TodoFrameCustom(el) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const lists = getLists();
|
const customLists = TodoLogic.getCustomTodoLists(todoData);
|
||||||
|
|
||||||
const container = el.querySelector('.container');
|
const container = el.querySelector('.container');
|
||||||
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 = lists.map((list) => {
|
const children = customLists.map((list) => {
|
||||||
let child = childrenByKey.get(list.id);
|
let child = childrenByKey.get(list.id);
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
@@ -93,7 +87,7 @@ export function TodoFrameCustom(el) {
|
|||||||
TodoCustomList(child);
|
TodoCustomList(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
child.dispatchEvent(new CustomEvent('todoCustomList', { detail: list }));
|
child.dispatchEvent(new CustomEvent('customTodoList', { detail: list }));
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
@@ -136,21 +130,4 @@ export function TodoFrameCustom(el) {
|
|||||||
// Update collapsible on changing heights
|
// Update collapsible on changing heights
|
||||||
el.dispatchEvent(new CustomEvent('collapse', { bubbles: true }));
|
el.dispatchEvent(new CustomEvent('collapse', { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLists() {
|
|
||||||
return todoData.customLists
|
|
||||||
.map((list) => ({
|
|
||||||
id: list.id,
|
|
||||||
index: list.index,
|
|
||||||
title: list.title,
|
|
||||||
items: getItemsForList(list.id),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.index - b.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemsForList(listId) {
|
|
||||||
return todoData.items
|
|
||||||
.filter((item) => item.listId === listId)
|
|
||||||
.sort((a, b) => a.index - b.index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,14 @@
|
|||||||
import { AppDatePicker } from './AppDatePicker.js';
|
import { AppDatePicker } from './AppDatePicker.js';
|
||||||
import { AppIcon } from './AppIcon.js';
|
import { AppIcon } from './AppIcon.js';
|
||||||
import { TodoDay } from './TodoDay.js';
|
import { TodoDay } from './TodoDay.js';
|
||||||
import { formatDateId } from './util.js';
|
import { TodoLogic } from './TodoLogic.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} el
|
* @param {HTMLElement} el
|
||||||
*/
|
*/
|
||||||
export function TodoFrameDays(el) {
|
export function TodoFrameDays(el) {
|
||||||
const RANGE = 14;
|
const RANGE = 14;
|
||||||
|
let todoData = TodoLogic.initTodoData();
|
||||||
let todoData = {
|
|
||||||
items: [],
|
|
||||||
at: formatDateId(new Date()),
|
|
||||||
};
|
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<nav class="leftcontrols">
|
<nav class="leftcontrols">
|
||||||
@@ -100,7 +96,7 @@ export function TodoFrameDays(el) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const days = getDays();
|
const listsByDay = TodoLogic.getTodoListsByDay(todoData, RANGE);
|
||||||
|
|
||||||
const container = el.querySelector('.container');
|
const container = el.querySelector('.container');
|
||||||
const obsolete = new Set(container.children);
|
const obsolete = new Set(container.children);
|
||||||
@@ -108,7 +104,7 @@ export function TodoFrameDays(el) {
|
|||||||
|
|
||||||
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
|
obsolete.forEach((child) => childrenByKey.set(child.dataset.key, child));
|
||||||
|
|
||||||
const children = days.map((day) => {
|
const children = listsByDay.map((day) => {
|
||||||
let child = childrenByKey.get(day.id);
|
let child = childrenByKey.get(day.id);
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
@@ -147,28 +143,4 @@ export function TodoFrameDays(el) {
|
|||||||
|
|
||||||
el.style.height = `${height + 50}px`;
|
el.style.height = `${height + 50}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDays() {
|
|
||||||
const days = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 2 * RANGE; ++i) {
|
|
||||||
const t = new Date(todoData.at);
|
|
||||||
t.setDate(t.getDate() - RANGE + i);
|
|
||||||
const id = formatDateId(t);
|
|
||||||
|
|
||||||
days.push({
|
|
||||||
id,
|
|
||||||
items: getItemsForDay(id),
|
|
||||||
position: -RANGE + i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return days;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemsForDay(dateId) {
|
|
||||||
return todoData.items
|
|
||||||
.filter((item) => item.listId === dateId)
|
|
||||||
.sort((a, b) => a.index - b.index);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -50,7 +50,7 @@ export function TodoItem(el) {
|
|||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('checkTodoItem', {
|
new CustomEvent('checkTodoItem', {
|
||||||
detail: {
|
detail: {
|
||||||
item,
|
...item,
|
||||||
done: !item.done,
|
done: !item.done,
|
||||||
},
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -121,9 +121,9 @@ export function TodoItem(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('saveTodoItem', {
|
new CustomEvent('editTodoItem', {
|
||||||
detail: {
|
detail: {
|
||||||
item,
|
...item,
|
||||||
label,
|
label,
|
||||||
},
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
@@ -20,7 +20,7 @@ export function TodoList(el) {
|
|||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
new CustomEvent('moveTodoItem', {
|
new CustomEvent('moveTodoItem', {
|
||||||
detail: {
|
detail: {
|
||||||
item: e.detail.data.item,
|
...e.detail.data.item,
|
||||||
index: e.detail.index,
|
index: e.detail.index,
|
||||||
},
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
212
public/scripts/TodoLogic.js
Normal file
212
public/scripts/TodoLogic.js
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { formatDateId } from './util.js';
|
||||||
|
import { uuid } from './uuid.js';
|
||||||
|
|
||||||
|
export class TodoLogic {
|
||||||
|
static initTodoData(now = new Date()) {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
customLists: [],
|
||||||
|
at: formatDateId(now),
|
||||||
|
customAt: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTodoListsByDay(data, range) {
|
||||||
|
const listsByDay = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 2 * range; ++i) {
|
||||||
|
const t = new Date(data.at);
|
||||||
|
t.setDate(t.getDate() - range + i);
|
||||||
|
const id = formatDateId(t);
|
||||||
|
|
||||||
|
listsByDay.push({
|
||||||
|
id,
|
||||||
|
items: TodoLogic.getTodoItemsForList(data, id),
|
||||||
|
position: -range + i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return listsByDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTodoItemsForList(data, listId) {
|
||||||
|
return data.items
|
||||||
|
.filter((item) => item.listId === listId)
|
||||||
|
.sort((a, b) => a.index - b.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static addTodoItem(data, input) {
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
for (const item of data.items) {
|
||||||
|
if (item.listId === input.listId) {
|
||||||
|
index = Math.max(index, item.index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
items: [
|
||||||
|
...data.items,
|
||||||
|
{
|
||||||
|
...input,
|
||||||
|
id: uuid(),
|
||||||
|
index,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static checkTodoItem(data, input) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
items: data.items.map((item) =>
|
||||||
|
item.id === input.id ? { ...item, done: input.done } : item,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static editTodoItem(data, input) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
items: data.items.map((item) =>
|
||||||
|
item.id === input.id ? { ...item, label: input.label } : item,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static moveTodoItem(data, input) {
|
||||||
|
const itemToMove = data.items.find((item) => item.id === input.id);
|
||||||
|
|
||||||
|
// Reinsert item at target list and index
|
||||||
|
let list = data.items.filter(
|
||||||
|
(item) => item.listId === input.listId && item.id !== input.id,
|
||||||
|
);
|
||||||
|
list.splice(input.index, 0, { ...itemToMove, listId: input.listId });
|
||||||
|
list = TodoLogic.setIndexes(list);
|
||||||
|
|
||||||
|
// Reinsert updated list
|
||||||
|
let items = data.items.filter(
|
||||||
|
(item) => item.listId !== input.listId && item.id !== input.id,
|
||||||
|
);
|
||||||
|
items = [...items, ...list];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteTodoItem(data, input) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
items: data.items.filter((item) => item.id !== input.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
static getCustomTodoLists(data) {
|
||||||
|
return data.customLists
|
||||||
|
.map((list) => ({
|
||||||
|
id: list.id,
|
||||||
|
index: list.index,
|
||||||
|
title: list.title,
|
||||||
|
items: TodoLogic.getTodoItemsForList(data, list.id),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.index - b.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static addCustomTodoList(data) {
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
for (const customList of data.customLists) {
|
||||||
|
index = Math.max(index, customList.index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
customLists: [
|
||||||
|
...data.customLists,
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
index,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static editCustomTodoList(data, input) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
customLists: data.customLists.map((customList) =>
|
||||||
|
customList.id === input.id
|
||||||
|
? { ...customList, title: input.title }
|
||||||
|
: customList,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static moveCustomTodoList(data, input) {
|
||||||
|
const customListToMove = data.customLists.find(
|
||||||
|
(customList) => customList.id === input.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let customLists = data.customLists
|
||||||
|
.filter((customList) => customList.id !== input.id)
|
||||||
|
.sort((a, b) => a.index - b.index);
|
||||||
|
customLists.splice(input.index, 0, customListToMove);
|
||||||
|
customLists = TodoLogic.setIndexes(customLists);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
customLists,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteCustomTodoList(data, input) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
customLists: data.customLists.filter(
|
||||||
|
(customList) => customList.id !== input.id,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
static seekDays(data, delta) {
|
||||||
|
const t = new Date(`${data.at}T00:00:00`);
|
||||||
|
t.setDate(t.getDate() + delta);
|
||||||
|
|
||||||
|
return { ...data, at: formatDateId(t) };
|
||||||
|
}
|
||||||
|
|
||||||
|
static seekToToday(data) {
|
||||||
|
return { ...data, at: formatDateId(new Date()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
static seekToDate(data, date) {
|
||||||
|
return { ...data, at: formatDateId(date) };
|
||||||
|
}
|
||||||
|
|
||||||
|
static seekCustomTodoLists(data, delta) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
customAt: Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(data.customLists.length - 1, data.customAt + delta),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
static setIndexes(array) {
|
||||||
|
return array.map((item, index) =>
|
||||||
|
item.index === index ? item : { ...item, index },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,158 +1,36 @@
|
|||||||
import { formatDateId, uuid } from './util.js';
|
import { TodoLogic } from './TodoLogic.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} el
|
* @param {HTMLElement} el
|
||||||
*/
|
*/
|
||||||
export function TodoStore(el) {
|
export function TodoStore(el) {
|
||||||
const todoData = {
|
let todoData = TodoLogic.initTodoData();
|
||||||
items: [],
|
let saveTimeout;
|
||||||
customLists: [],
|
|
||||||
at: formatDateId(new Date()),
|
|
||||||
customAt: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let storeTimeout;
|
|
||||||
|
|
||||||
el.addEventListener('loadTodoStore', load);
|
el.addEventListener('loadTodoStore', load);
|
||||||
|
|
||||||
el.addEventListener('addTodoItem', (e) => {
|
for (const action of [
|
||||||
let index = 0;
|
'addTodoItem',
|
||||||
|
'checkTodoItem',
|
||||||
for (const item of todoData.items) {
|
'editTodoItem',
|
||||||
if (item.listId === e.detail.listId) {
|
'moveTodoItem',
|
||||||
index = Math.max(index, item.index + 1);
|
'deleteTodoItem',
|
||||||
}
|
'addCustomTodoList',
|
||||||
}
|
'editCustomTodoList',
|
||||||
|
'moveCustomTodoList',
|
||||||
todoData.items.push({
|
'deleteCustomTodoList',
|
||||||
id: uuid(),
|
'seekDays',
|
||||||
listId: e.detail.listId,
|
'seekToToday',
|
||||||
index,
|
'seekToDate',
|
||||||
label: e.detail.label,
|
'seekCustomTodoLists',
|
||||||
done: false,
|
]) {
|
||||||
|
el.addEventListener(action, (e) => {
|
||||||
|
todoData = TodoLogic[action](todoData, e.detail);
|
||||||
|
update();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({ items: todoData.items });
|
function update() {
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('checkTodoItem', (e) => {
|
|
||||||
if (e.detail.item.done === e.detail.done) return;
|
|
||||||
|
|
||||||
e.detail.item.done = e.detail.done;
|
|
||||||
dispatch({ items: todoData.items });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('saveTodoItem', (e) => {
|
|
||||||
if (e.detail.item.label === e.detail.label) return;
|
|
||||||
|
|
||||||
e.detail.item.label = e.detail.label;
|
|
||||||
dispatch({ items: todoData.items });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('moveTodoItem', (e) => {
|
|
||||||
const movedItem = todoData.items.find(
|
|
||||||
(item) => item.id === e.detail.item.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const listItems = todoData.items.filter(
|
|
||||||
(item) => item.listId === e.detail.listId && item !== movedItem,
|
|
||||||
);
|
|
||||||
|
|
||||||
listItems.sort((a, b) => a.index - b.index);
|
|
||||||
|
|
||||||
movedItem.listId = e.detail.listId;
|
|
||||||
listItems.splice(e.detail.index, 0, movedItem);
|
|
||||||
|
|
||||||
listItems.forEach((item, index) => {
|
|
||||||
item.index = index;
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({ items: todoData.items });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('deleteTodoItem', (e) =>
|
|
||||||
dispatch({
|
|
||||||
items: todoData.items.filter((item) => item.id !== e.detail.id),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
el.addEventListener('addTodoList', (e) => {
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
for (const customList of todoData.customLists) {
|
|
||||||
index = Math.max(index, customList.index + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
todoData.customLists.push({
|
|
||||||
id: uuid(),
|
|
||||||
index,
|
|
||||||
title: e.detail.title || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({ customLists: todoData.customLists });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('saveTodoList', (e) => {
|
|
||||||
const list = todoData.customLists.find((l) => l.id === e.detail.list.id);
|
|
||||||
|
|
||||||
if (list.title === e.detail.title) return;
|
|
||||||
|
|
||||||
list.title = e.detail.title;
|
|
||||||
|
|
||||||
dispatch({ customLists: todoData.customLists });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('moveTodoList', (e) => {
|
|
||||||
const movedListIndex = todoData.customLists.findIndex(
|
|
||||||
(list) => list.id === e.detail.list.id,
|
|
||||||
);
|
|
||||||
const movedList = todoData.customLists[movedListIndex];
|
|
||||||
|
|
||||||
todoData.customLists.splice(movedListIndex, 1);
|
|
||||||
todoData.customLists.sort((a, b) => a.index - b.index);
|
|
||||||
todoData.customLists.splice(e.detail.index, 0, movedList);
|
|
||||||
|
|
||||||
todoData.customLists.forEach((item, index) => {
|
|
||||||
item.index = index;
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({ customLists: todoData.customLists });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('deleteTodoList', (e) =>
|
|
||||||
dispatch({
|
|
||||||
customLists: todoData.customLists.filter(
|
|
||||||
(customList) => customList.id !== e.detail.id,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
el.addEventListener('seekDays', (e) => {
|
|
||||||
const t = new Date(`${todoData.at}T00:00:00`);
|
|
||||||
t.setDate(t.getDate() + e.detail);
|
|
||||||
|
|
||||||
dispatch({ at: formatDateId(t) });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('seekToToday', () =>
|
|
||||||
dispatch({ at: formatDateId(new Date()) }),
|
|
||||||
);
|
|
||||||
|
|
||||||
el.addEventListener('seekToDate', (e) =>
|
|
||||||
dispatch({ at: formatDateId(e.detail) }),
|
|
||||||
);
|
|
||||||
|
|
||||||
el.addEventListener('seekCustomTodoLists', (e) =>
|
|
||||||
dispatch({
|
|
||||||
customAt: Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(todoData.customLists.length - 1, todoData.customAt + e.detail),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
function dispatch(next) {
|
|
||||||
Object.assign(todoData, next);
|
|
||||||
save();
|
save();
|
||||||
|
|
||||||
el.dispatchEvent(
|
el.dispatchEvent(
|
||||||
@@ -164,23 +42,22 @@ export function TodoStore(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
if (!localStorage || !localStorage.todo) {
|
|
||||||
dispatch(todoData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dispatch(JSON.parse(localStorage.todo));
|
if (localStorage?.todo) {
|
||||||
|
todoData = { ...todoData, ...JSON.parse(localStorage.todo) };
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
clearTimeout(storeTimeout);
|
clearTimeout(saveTimeout);
|
||||||
|
|
||||||
storeTimeout = setTimeout(() => {
|
saveTimeout = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
localStorage.todo = JSON.stringify(todoData);
|
localStorage.todo = JSON.stringify(todoData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@@ -1,12 +1,3 @@
|
|||||||
export function uuid() {
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
||||||
const r = (Math.random() * 16) | 0,
|
|
||||||
v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
||||||
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Date} date
|
* @param {Date} date
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
8
public/scripts/uuid.js
Normal file
8
public/scripts/uuid.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function uuid() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = (Math.random() * 16) | 0,
|
||||||
|
v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||||
|
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
12
test/e2e/addCustomTodoList.test.mjs
Normal file
12
test/e2e/addCustomTodoList.test.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import '../coverage.mjs';
|
||||||
|
|
||||||
|
test('Add custom to-do list', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080');
|
||||||
|
|
||||||
|
const add = page.locator('.todo-frame.-custom .add');
|
||||||
|
await add.click();
|
||||||
|
|
||||||
|
const title = page.locator('.todo-custom-list > .header > .title');
|
||||||
|
await expect(title).toHaveText('...');
|
||||||
|
});
|
20
test/e2e/deleteCustomTodoList.test.mjs
Normal file
20
test/e2e/deleteCustomTodoList.test.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import '../coverage.mjs';
|
||||||
|
|
||||||
|
test('Delete custom to-do list', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:8080');
|
||||||
|
|
||||||
|
const add = page.locator('.todo-frame.-custom .add');
|
||||||
|
await add.click();
|
||||||
|
|
||||||
|
const title = page.locator('.todo-custom-list > .header > .title');
|
||||||
|
await title.click();
|
||||||
|
|
||||||
|
const button = page.locator('.todo-custom-list > .header > .form > .delete');
|
||||||
|
|
||||||
|
await expect(button).toBeVisible();
|
||||||
|
|
||||||
|
await button.click();
|
||||||
|
|
||||||
|
await expect(title).not.toBeAttached();
|
||||||
|
});
|
@@ -1,16 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import '../coverage.mjs';
|
import '../coverage.mjs';
|
||||||
|
|
||||||
test('Add custom to-do list', async ({ page }) => {
|
|
||||||
await page.goto('http://localhost:8080');
|
|
||||||
|
|
||||||
const add = page.locator('.todo-frame.-custom .add');
|
|
||||||
await add.click();
|
|
||||||
|
|
||||||
const title = page.locator('.todo-custom-list > .header > .title');
|
|
||||||
await expect(title).toHaveText('...');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Edit custom to-do list title (Enter)', async ({ page }) => {
|
test('Edit custom to-do list title (Enter)', async ({ page }) => {
|
||||||
await page.goto('http://localhost:8080');
|
await page.goto('http://localhost:8080');
|
||||||
|
|
143
test/unit/TodoLogic.test.mjs
Normal file
143
test/unit/TodoLogic.test.mjs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import TodoLogicModule from '../../public/scripts/TodoLogic.js';
|
||||||
|
import '../coverage.mjs';
|
||||||
|
|
||||||
|
const { TodoLogic } = TodoLogicModule;
|
||||||
|
|
||||||
|
test('TodoLogic.initTodoData', () => {
|
||||||
|
const data = TodoLogic.initTodoData(new Date(0));
|
||||||
|
|
||||||
|
expect(data).toEqual({
|
||||||
|
at: '1970-01-01',
|
||||||
|
customAt: 0,
|
||||||
|
customLists: [],
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TodoLogic.addTodoItem', () => {
|
||||||
|
let data = TodoLogic.initTodoData(new Date(0));
|
||||||
|
|
||||||
|
data = TodoLogic.addTodoItem(data, { label: 'foo', listId: '1970-01-01' });
|
||||||
|
|
||||||
|
expect(data).toEqual({
|
||||||
|
at: '1970-01-01',
|
||||||
|
customAt: 0,
|
||||||
|
customLists: [],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(/./),
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'foo',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
data = TodoLogic.addTodoItem(data, { label: 'bar', listId: '1970-01-01' });
|
||||||
|
|
||||||
|
expect(data.items).toEqual([
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(/./),
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'foo',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(/./),
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'bar',
|
||||||
|
index: 1,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
data = TodoLogic.addTodoItem(data, { label: 'baz', listId: '1970-01-02' });
|
||||||
|
|
||||||
|
expect(data.items).toEqual([
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(/./),
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'foo',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(/./),
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'bar',
|
||||||
|
index: 1,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.stringMatching(/./),
|
||||||
|
listId: '1970-01-02',
|
||||||
|
label: 'baz',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TodoLogic.moveTodoItem', () => {
|
||||||
|
let data = TodoLogic.initTodoData(new Date(0));
|
||||||
|
|
||||||
|
data = {
|
||||||
|
...data,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'a',
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'foo',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b',
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'bar',
|
||||||
|
index: 1,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c',
|
||||||
|
listId: '1970-01-02',
|
||||||
|
label: 'baz',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
data = TodoLogic.moveTodoItem(data, {
|
||||||
|
id: 'a',
|
||||||
|
listId: '1970-01-01',
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data.items).toEqual([
|
||||||
|
{
|
||||||
|
id: 'c',
|
||||||
|
listId: '1970-01-02',
|
||||||
|
label: 'baz',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b',
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'bar',
|
||||||
|
index: 0,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a',
|
||||||
|
listId: '1970-01-01',
|
||||||
|
label: 'foo',
|
||||||
|
index: 1,
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
Reference in New Issue
Block a user