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=', + ); +});