mirror of
https://github.com/morris/vanilla-todo.git
synced 2025-08-20 04:41:26 +02:00
358 lines
7.3 KiB
JavaScript
358 lines
7.3 KiB
JavaScript
import { formatDateId } from './util.js';
|
|
import { uuid } from './uuid.js';
|
|
|
|
/**
|
|
* @typedef {{
|
|
* id: string;
|
|
* listId: string;
|
|
* index: number;
|
|
* label: string;
|
|
* done: boolean;
|
|
* fixed: boolean;
|
|
* }} TodoDataItem
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* id: string;
|
|
* index: number;
|
|
* title: string;
|
|
* }} TodoDataCustomList
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{
|
|
* items: TodoDataItem[];
|
|
* customLists: TodoDataCustomList[];
|
|
* at: string;
|
|
* customAt: number;
|
|
* }} TodoData
|
|
*/
|
|
|
|
export class TodoLogic {
|
|
/**
|
|
* @param {Date} now
|
|
* @returns {TodoData}
|
|
*/
|
|
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}T00:00:00`);
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{listId: string, label: string}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
static addTodoItem(data, input, now = new Date()) {
|
|
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,
|
|
fixed: this.isListInThePast(input.listId, now),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{id: string, done: boolean}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
static checkTodoItem(data, input) {
|
|
return {
|
|
...data,
|
|
items: data.items.map((item) =>
|
|
item.id === input.id ? { ...item, done: input.done } : item,
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{id: string, label: string}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
static editTodoItem(data, input) {
|
|
return {
|
|
...data,
|
|
items: data.items.map((item) =>
|
|
item.id === input.id ? { ...item, label: input.label } : item,
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{id: string, listId: string, index: number}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
static moveTodoItem(data, input, now = new Date()) {
|
|
const itemToMove = data.items.find((item) => item.id === input.id);
|
|
|
|
if (!itemToMove) return data;
|
|
|
|
// 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,
|
|
fixed: this.isListInThePast(input.listId, now),
|
|
});
|
|
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,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{id: string}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
static deleteTodoItem(data, input) {
|
|
return {
|
|
...data,
|
|
items: data.items.filter((item) => item.id !== input.id),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {Date} now
|
|
* @returns {TodoData}
|
|
*/
|
|
static movePastTodoItems(data, now = new Date()) {
|
|
const todayListId = formatDateId(now);
|
|
|
|
let targetIndex = 0;
|
|
|
|
for (const item of data.items) {
|
|
if (item.listId === todayListId && item.index > targetIndex) {
|
|
targetIndex = item.index;
|
|
}
|
|
}
|
|
|
|
return {
|
|
...data,
|
|
items: data.items.map((item) => {
|
|
if (
|
|
!item.done &&
|
|
!item.fixed &&
|
|
this.isListInThePast(item.listId, now)
|
|
) {
|
|
return { ...item, listId: todayListId, index: targetIndex++ };
|
|
}
|
|
|
|
return item;
|
|
}),
|
|
};
|
|
}
|
|
|
|
static isListInThePast(listId, now = new Date()) {
|
|
const todayListId = formatDateId(now);
|
|
|
|
return listId.match(/\d\d\d\d-\d\d-\d\d/) && listId < todayListId;
|
|
}
|
|
|
|
//
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @returns {TodoData}
|
|
*/
|
|
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: '',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{id: string, title: string}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
static editCustomTodoList(data, input) {
|
|
return {
|
|
...data,
|
|
customLists: data.customLists.map((customList) =>
|
|
customList.id === input.id
|
|
? { ...customList, title: input.title }
|
|
: customList,
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{id: string, index: number}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
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,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {{id: string}} input
|
|
* @returns {TodoData}
|
|
*/
|
|
static deleteCustomTodoList(data, input) {
|
|
return {
|
|
...data,
|
|
customLists: data.customLists.filter(
|
|
(customList) => customList.id !== input.id,
|
|
),
|
|
};
|
|
}
|
|
|
|
//
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {number} delta
|
|
* @returns {TodoData}
|
|
*/
|
|
static seekDays(data, delta) {
|
|
const t = new Date(`${data.at}T00:00:00`);
|
|
t.setDate(t.getDate() + delta);
|
|
|
|
return { ...data, at: formatDateId(t) };
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @returns {TodoData}
|
|
*/
|
|
static seekToToday(data) {
|
|
return { ...data, at: formatDateId(new Date()) };
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {Date} date
|
|
* @returns {TodoData}
|
|
*/
|
|
static seekToDate(data, date) {
|
|
return { ...data, at: formatDateId(date) };
|
|
}
|
|
|
|
/**
|
|
* @param {TodoData} data
|
|
* @param {number} delta
|
|
* @returns {TodoData}
|
|
*/
|
|
static seekCustomTodoLists(data, delta) {
|
|
return {
|
|
...data,
|
|
customAt: Math.max(
|
|
0,
|
|
Math.min(data.customLists.length - 1, data.customAt + delta),
|
|
),
|
|
};
|
|
}
|
|
|
|
//
|
|
|
|
/**
|
|
* @template {{index?: number}} T
|
|
* @param {T[]} array
|
|
* @returns {T[]}
|
|
*/
|
|
static setIndexes(array) {
|
|
return array.map((item, index) =>
|
|
item.index === index ? item : { ...item, index },
|
|
);
|
|
}
|
|
}
|