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:
@@ -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);
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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 }));
|
||||
});
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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=',
|
||||
);
|
||||
});
|
||||
|
Reference in New Issue
Block a user