mirror of
https://github.com/morris/vanilla-todo.git
synced 2025-08-21 21:25:25 +02:00
refactor state
This commit is contained in:
59
README.md
59
README.md
@@ -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);
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
},
|
};
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(next) {
|
update();
|
||||||
Object.assign(state, next);
|
}
|
||||||
|
|
||||||
el.classList.toggle('-show', state.show);
|
function update() {
|
||||||
|
el.classList.toggle('-show', 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,
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -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()),
|
||||||
@@ -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) =>
|
||||||
|
@@ -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;
|
||||||
|
@@ -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 }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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);
|
||||||
|
Reference in New Issue
Block a user