diff --git a/public/scripts/TodoApp.js b/public/scripts/TodoApp.js
index e9fb8e7..cc1ee0a 100644
--- a/public/scripts/TodoApp.js
+++ b/public/scripts/TodoApp.js
@@ -17,6 +17,15 @@ export function TodoApp(el) {
VANILLA TODO
+
+
+
+
@@ -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);
diff --git a/public/scripts/TodoController.js b/public/scripts/TodoController.js
index 7ef1b28..61f5502 100644
--- a/public/scripts/TodoController.js
+++ b/public/scripts/TodoController.js
@@ -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();
+ }
}
diff --git a/public/scripts/util.js b/public/scripts/util.js
index 4f61169..2bda634 100644
--- a/public/scripts/util.js
+++ b/public/scripts/util.js
@@ -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 }));
+ });
+}
diff --git a/public/styles/app-header.css b/public/styles/app-header.css
index 4f2f26f..6a14110 100644
--- a/public/styles/app-header.css
+++ b/public/styles/app-header.css
@@ -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;
+}
diff --git a/test/unit/util.test.js b/test/unit/util.test.js
index b386bcd..5184613 100644
--- a/test/unit/util.test.js
+++ b/test/unit/util.test.js
@@ -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=',
+ );
+});