1
0
mirror of https://github.com/morris/vanilla-todo.git synced 2025-08-19 04:11:18 +02:00

add data import/export (#11)

This commit is contained in:
Morris Brodersen
2024-06-24 07:25:56 +02:00
parent 7204404fdc
commit 46932737bc
5 changed files with 101 additions and 0 deletions

View File

@@ -17,6 +17,15 @@ export function TodoApp(el) {
<h1 class="title">
VANILLA TODO
</h1>
<p class="actions">
<label class="app-button import" title="Import data">
<i class="app-icon" data-id="upload-16"></i>
<input type="file" name="importFile" hidden>
</label>
<button class="app-button export" title="Export data">
<i class="app-icon" data-id="download-16"></i>
</button>
</p>
</header>
<div class="todo-frame -days"></div>
<div class="app-collapsible">
@@ -51,6 +60,37 @@ export function TodoApp(el) {
TodoFrameDays(el.querySelector('.todo-frame.-days'));
TodoFrameCustom(el.querySelector('.todo-frame.-custom'));
el.querySelector('[name=importFile]').addEventListener('change', (e) => {
const f = e.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.addEventListener('load', (e) => {
try {
const todoData = JSON.parse(e.target.result);
el.dispatchEvent(
new CustomEvent('importTodoData', {
detail: todoData,
bubbles: true,
}),
);
} catch (err) {
alert(`Could not import data (${err.message})`);
}
});
reader.readAsText(f);
});
el.querySelector('.app-header > .actions > .export').addEventListener(
'click',
() => {
el.dispatchEvent(new CustomEvent('exportTodoData', { bubbles: true }));
},
);
// Each of these events make changes to the HTML to be animated using FLIP.
// Listening to them using "capture" dispatches "beforeFlip" before any changes.
el.addEventListener('todoData', beforeFlip, true);

View File

@@ -1,4 +1,5 @@
import { TodoLogic } from './TodoLogic.js';
import { toDataURL } from './util.js';
/**
* @param {HTMLElement} el
@@ -8,6 +9,8 @@ export function TodoController(el) {
let saveTimeout;
el.addEventListener('loadTodoData', load);
el.addEventListener('importTodoData', (e) => importTodoData(e.detail));
el.addEventListener('exportTodoData', exportTodoData);
for (const action of [
'addTodoItem',
@@ -69,4 +72,22 @@ export function TodoController(el) {
}
}, 100);
}
function importTodoData(input) {
// TODO validate?
todoData = input;
update();
}
async function exportTodoData() {
const json = JSON.stringify(todoData, null, 2);
const href = await toDataURL(json);
const link = document.createElement('a');
link.setAttribute('download', 'todo.json');
link.setAttribute('href', href);
document.querySelector('body').appendChild(link);
link.click();
link.remove();
}
}

View File

@@ -84,3 +84,16 @@ export const MONTH_NAMES = [
export function formatMonth(date) {
return MONTH_NAMES[date.getMonth()];
}
/**
* https://developer.mozilla.org/en-US/docs/Glossary/Base64
* @param {BlobPart} input
*/
export async function toDataURL(input, type) {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', () => reject(reader.error));
reader.readAsDataURL(new File([input], '', { type }));
});
}

View File

@@ -1,6 +1,7 @@
.app-header {
background: var(--header-bg);
padding: 10px 20px;
position: relative;
}
.app-header > .title {
@@ -10,3 +11,10 @@
margin: 0;
text-align: center;
}
.app-header > .actions {
position: absolute;
right: 20px;
top: 10px;
margin: 0;
}

View File

@@ -6,3 +6,22 @@ test('formatDate', () => {
expect(formatDate(new Date(0))).toEqual('January 1st 1970');
expect(formatDate(new Date('2023-05-13 12:00:00'))).toEqual('May 13th 2023');
});
test('toDataURL', async ({ page }) => {
// Needs to be tested in the browser because FileReader is not available in Node.js
// However, this approach does not support test coverage :'(
await page.goto('http://localhost:8080');
const dataURL = await page.evaluate(async () => {
const { toDataURL } = await import('./scripts/util.js');
const text = 'a Ā 𐀀 文 🦄';
const json = JSON.stringify({ text });
const dataURL = await toDataURL(json, 'application/json;charset=utf-8');
return dataURL;
});
expect(dataURL).toEqual(
'data:application/json;charset=utf-8;base64,eyJ0ZXh0IjoiYSDEgCDwkICAIOaWhyDwn6aEIn0=',
);
});