mirror of
https://github.com/morris/vanilla-todo.git
synced 2025-08-19 12:21:19 +02:00
add data import/export (#11)
This commit is contained in:
@@ -17,6 +17,15 @@ export function TodoApp(el) {
|
|||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
VANILLA TODO
|
VANILLA TODO
|
||||||
</h1>
|
</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>
|
</header>
|
||||||
<div class="todo-frame -days"></div>
|
<div class="todo-frame -days"></div>
|
||||||
<div class="app-collapsible">
|
<div class="app-collapsible">
|
||||||
@@ -51,6 +60,37 @@ export function TodoApp(el) {
|
|||||||
TodoFrameDays(el.querySelector('.todo-frame.-days'));
|
TodoFrameDays(el.querySelector('.todo-frame.-days'));
|
||||||
TodoFrameCustom(el.querySelector('.todo-frame.-custom'));
|
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.
|
// Each of these events make changes to the HTML to be animated using FLIP.
|
||||||
// Listening to them using "capture" dispatches "beforeFlip" before any changes.
|
// Listening to them using "capture" dispatches "beforeFlip" before any changes.
|
||||||
el.addEventListener('todoData', beforeFlip, true);
|
el.addEventListener('todoData', beforeFlip, true);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { TodoLogic } from './TodoLogic.js';
|
import { TodoLogic } from './TodoLogic.js';
|
||||||
|
import { toDataURL } from './util.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} el
|
* @param {HTMLElement} el
|
||||||
@@ -8,6 +9,8 @@ export function TodoController(el) {
|
|||||||
let saveTimeout;
|
let saveTimeout;
|
||||||
|
|
||||||
el.addEventListener('loadTodoData', load);
|
el.addEventListener('loadTodoData', load);
|
||||||
|
el.addEventListener('importTodoData', (e) => importTodoData(e.detail));
|
||||||
|
el.addEventListener('exportTodoData', exportTodoData);
|
||||||
|
|
||||||
for (const action of [
|
for (const action of [
|
||||||
'addTodoItem',
|
'addTodoItem',
|
||||||
@@ -69,4 +72,22 @@ export function TodoController(el) {
|
|||||||
}
|
}
|
||||||
}, 100);
|
}, 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) {
|
export function formatMonth(date) {
|
||||||
return MONTH_NAMES[date.getMonth()];
|
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 {
|
.app-header {
|
||||||
background: var(--header-bg);
|
background: var(--header-bg);
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header > .title {
|
.app-header > .title {
|
||||||
@@ -10,3 +11,10 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
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(0))).toEqual('January 1st 1970');
|
||||||
expect(formatDate(new Date('2023-05-13 12:00:00'))).toEqual('May 13th 2023');
|
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