Skip to content

Commit 7448b92

Browse files
committed
Add optional progress bar to lists
* Progress bars are disabled by default * Add config setting to make nav bar compact * Refactor Settings component
1 parent 9c95732 commit 7448b92

File tree

5 files changed

+261
-93
lines changed

5 files changed

+261
-93
lines changed

assets/components.js

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ window.LIST_EVENTS = {
99
DELETE: 'list:delete',
1010
}
1111

12-
function dispatchTaskEvent(eventName, task, isDone) {
12+
function dispatchTaskEvent(eventName, task, isDone, target) {
13+
target = target || document
1314
const taskChangeEvent = new CustomEvent(eventName, { detail: { task: task, done: isDone } })
1415

15-
document.dispatchEvent(taskChangeEvent)
16+
target.dispatchEvent(taskChangeEvent)
1617
}
1718

1819
/**
@@ -32,7 +33,7 @@ class TaskControl extends HTMLElement {
3233

3334
get form() { return this.querySelector('form') }
3435

35-
get taskCategory() { return Sortable.utils.closest(this, `.${TaskCategory.TAG}`) }
36+
get taskCategory() { return Sortable.utils.closest(this, TaskCategory.TAG) }
3637

3738
setup() {
3839
this.on(this.form, 'submit', this._submitHandler.bind(this))
@@ -46,6 +47,7 @@ class TaskControl extends HTMLElement {
4647

4748
taskItem = this.taskCategory.addTask(taskItem)
4849
dispatchTaskEvent(TASK_EVENTS.CHANGE, task, false)
50+
dispatchTaskEvent(TASK_EVENTS.CHANGE, task, false, this.taskCategory)
4951

5052
return taskItem
5153
}
@@ -94,6 +96,8 @@ class TaskItem extends HTMLElement {
9496

9597
get task() { return this.getAttribute('name') }
9698

99+
get taskCategory() { return Sortable.utils.closest(this, TaskCategory.TAG) }
100+
97101
setup() {
98102
if (this.hasAttribute('done') && this.getAttribute('done') !== 'false') {
99103
this.done = true
@@ -111,12 +115,14 @@ class TaskItem extends HTMLElement {
111115

112116
// order here is important, bubble up event before changing internal state
113117
dispatchTaskEvent(TASK_EVENTS.CHANGE, this.name, checked)
118+
dispatchTaskEvent(TASK_EVENTS.CHANGE, this.name, checked, this.taskCategory)
114119
this.done = checked
115120
}
116121

117122
_deleteHandler() {
118123
this.remove()
119124
dispatchTaskEvent(TASK_EVENTS.DELETE, this.name)
125+
dispatchTaskEvent(TASK_EVENTS.DELETE, this.name, checked, this.taskCategory)
120126
}
121127
}
122128

@@ -142,6 +148,10 @@ class TaskCategory extends HTMLElement {
142148

143149
get deleteButton() { return this.querySelector('.delete') }
144150

151+
get progress() { return this.querySelector('progress') }
152+
153+
get progressLabel() { return this.querySelector('.progress-label') }
154+
145155
get name() { return this.getAttribute('name').trim() }
146156

147157
get nameLabel() { return this.querySelector('.category--name') }
@@ -151,15 +161,10 @@ class TaskCategory extends HTMLElement {
151161
get tasksContainer() { return this.querySelector('.tasks-container') }
152162

153163
get tasks() {
154-
const _tasks = []
155-
156-
this.querySelectorAll(TaskItem.TAG)
157-
.forEach((task) => _tasks.push({ done: !!task.done, name: task.name }))
158-
159-
return _tasks;
164+
return Array.from(this.querySelectorAll(TaskItem.TAG))
165+
.map(task => ({ done: !!task.done, name: task.name }))
160166
}
161167

162-
163168
static addList(target) {
164169
const listName = window.prompt(NEW_LIST_PROMPT)
165170

@@ -179,6 +184,9 @@ class TaskCategory extends HTMLElement {
179184

180185
this.on(this.colorPicker, 'change', this._colorChangeHandler.bind(this))
181186
this.on(this.deleteButton, 'click', this._deleteHandler.bind(this))
187+
this.on(this, TASK_EVENTS.CHANGE, this._updateProgress.bind(this))
188+
this.on(this, TASK_EVENTS.DELETE, this._updateProgress.bind(this))
189+
this._updateProgress()
182190
}
183191

184192
addTask(taskItem) {
@@ -196,6 +204,18 @@ class TaskCategory extends HTMLElement {
196204
document.dispatchEvent(new CustomEvent(LIST_EVENTS.DELETE, { detail: { name: this.name } }))
197205
}
198206
}
207+
208+
_updateProgress(ev) {
209+
// give a few milliseconds to the browser to update elements
210+
window.setTimeout(() => {
211+
const _tasks = this.tasks
212+
const doneAmount = _tasks.filter(task => task.done).length
213+
const percent = (doneAmount / _tasks.length) * 100
214+
215+
this.progress.value = percent
216+
this.progressLabel.textContent = `${percent.toFixed(2)}%`
217+
}, 100)
218+
}
199219
}
200220

201221
class ColorPicker extends HTMLElement {
@@ -268,11 +288,11 @@ class ColorPicker extends HTMLElement {
268288
}
269289
}
270290

271-
class BackendSettings extends HTMLElement {
291+
class TodoSettings extends HTMLElement {
272292
static EXTENDED_ELEMENT = 'article'
273-
static TAG = 'backend-settings'
274-
static TEMPLATE_ID = 'backendsettings-template'
275-
static BACKEND_CHANGE_EVENT = 'backend:change'
293+
static TAG = 'todo-settings'
294+
static TEMPLATE_ID = 'todo-settings-template'
295+
static SETTINGS_CHANGE_EVENT = 'settings:change'
276296

277297
get form() { return this.querySelector('form') }
278298

@@ -288,6 +308,9 @@ class BackendSettings extends HTMLElement {
288308
get passphrase() { return this.querySelector('form textarea[name=backend_passphrase]') }
289309
set passphrase(newVal) { this.querySelector('form textarea[name=backend_passphrase]').value = newVal }
290310

311+
get navCompact() { return this.querySelector('#nav_compact') }
312+
get tasksProgress() { return this.querySelector('#tasks_progress') }
313+
291314
/**
292315
* Setup controls reactivity:
293316
*
@@ -300,10 +323,9 @@ class BackendSettings extends HTMLElement {
300323
*/
301324
setup() {
302325
this.setupAutoStorage()
303-
this.on(this.url, 'input', this._inputHandler.bind(this))
304-
this.on(this.username, 'input', this._inputHandler.bind(this))
305-
this.on(this.passphrase, 'input', this._inputHandler.bind(this))
306-
this.on(this.enabled, 'input', this._inputHandler.bind(this))
326+
Array.from(this.querySelectorAll('input')).forEach((el) => {
327+
this.on(el, 'input', this._inputHandler.bind(this))
328+
})
307329
this.on(this.form, 'submit', this._submitHandler.bind(this))
308330
}
309331

@@ -324,16 +346,33 @@ class BackendSettings extends HTMLElement {
324346

325347
emitConfig() {
326348
const payload = {
327-
url: this.url.value,
328-
username: this.username.value,
329-
passphrase: this.passphrase.value,
330-
enabled: !!this.enabled.checked,
349+
backend: {
350+
enabled: !!this.enabled.checked,
351+
passphrase: this.passphrase.value,
352+
username: this.username.value,
353+
url: this.url.value,
354+
},
355+
nav: {
356+
compact: this.navCompact.checked,
357+
},
358+
tasks: {
359+
progress: this.tasksProgress.checked,
360+
},
331361
}
332-
const newEv = new CustomEvent(this.constructor.BACKEND_CHANGE_EVENT, { detail: payload })
362+
const newEv = new CustomEvent(this.constructor.SETTINGS_CHANGE_EVENT, { detail: payload })
333363

334364
document.dispatchEvent(newEv)
335365
}
336366

367+
applyConfig(config) {
368+
this.enabled = config.backend?.enabled
369+
this.passphrase = config.backend?.passphrase
370+
this.url = config.backend?.url
371+
this.username = config.backend?.username
372+
this.navCompact.checked = !!config.nav?.compact
373+
this.tasksProgress.checked = !!config.tasks?.progress
374+
}
375+
337376
_inputHandler(ev) {
338377
this.emitConfig()
339378
}
@@ -411,7 +450,7 @@ class LogMessage extends HTMLElement {
411450

412451
function defineComponents() {
413452
const components = [
414-
BackendSettings,
453+
TodoSettings,
415454
ColorPicker,
416455
LogMessage,
417456
TaskCategory,

assets/lib.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ const INITIAL_STATE = {
1616
]
1717
}
1818

19+
const INITIAL_SETTINGS = {
20+
backend: {
21+
enabled: false,
22+
passphrase: null,
23+
username: null,
24+
url: null
25+
},
26+
nav: {
27+
compact: false
28+
},
29+
tasks: {
30+
progress: false
31+
}
32+
}
33+
1934
// https://javascript.info/mixins
2035
// Object.assign(ComponentClass, CustomElementStaticMixin)
2136
const CustomElementStaticMixin = {
@@ -344,6 +359,17 @@ class StateManager {
344359
StateManager.persistState(newState)
345360
return StateManager.loadState()
346361
}
362+
363+
static settings() {
364+
const storageState = localStorage.getItem('todoSettings')
365+
const settings = storageState ? JSON.parse(storageState) : INITIAL_SETTINGS
366+
367+
return settings
368+
}
369+
370+
static persistSettings(settings) {
371+
localStorage.setItem('todoSettings', JSON.stringify(settings))
372+
}
347373
}
348374

349375
function serializeString(str) {

assets/main.js

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,21 +118,12 @@ function init(ev) {
118118
if (window.DEBUG_MODE)
119119
setupDebugControls()
120120

121-
function stateUpdated() {
122-
const newState = StateManager.updateState()
123-
setupSortable()
124-
125-
postToBackend(newState)
126-
}
127-
128121
document.addEventListener(LIST_EVENTS.CHANGE, stateUpdated)
129122
document.addEventListener(LIST_EVENTS.DELETE, ev => { removeListLink(ev) ; stateUpdated() })
130123
document.addEventListener(TASK_EVENTS.CHANGE, stateUpdated)
131124
document.addEventListener(TASK_EVENTS.DELETE, stateUpdated)
132125

133-
document.addEventListener(BackendSettings.BACKEND_CHANGE_EVENT, (ev) => {
134-
window.backendClient = new BackendClient(ev.detail.url, ev.detail.username, ev.detail.passphrase, ev.detail.enabled)
135-
})
126+
document.addEventListener(TodoSettings.SETTINGS_CHANGE_EVENT, (ev) => applySettings(ev.detail))
136127

137128
document.querySelector('.new-list')?.addEventListener('click', ev => {
138129
const listName = TaskCategory.addList(document.getElementById('lists'))
@@ -147,9 +138,47 @@ function init(ev) {
147138
)
148139
document.querySelector('#import_json')?.addEventListener('change', importJson)
149140

141+
const todoSettings = document.querySelector(TodoSettings.TAG)
142+
const config = StateManager.settings()
143+
150144
StateManager.loadState()
145+
applySettings(config)
146+
151147
window.backendClient.get()
152148
setupSortable()
149+
150+
if (todoSettings) {
151+
todoSettings.applyConfig(config)
152+
}
153+
}
154+
155+
function stateUpdated() {
156+
const newState = StateManager.updateState()
157+
setupSortable()
158+
159+
postToBackend(newState)
160+
}
161+
162+
function applySettings(config) {
163+
const backend = config.backend
164+
165+
StateManager.persistSettings(config)
166+
167+
window.backendClient = new BackendClient(backend.url, backend.username, backend.passphrase, backend.enabled)
168+
169+
if (config.nav) {
170+
if (config.nav.compact)
171+
document.body.classList.add('config--nav-compact')
172+
else
173+
document.body.classList.remove('config--nav-compact')
174+
}
175+
176+
if (config.tasks) {
177+
if (config.tasks.progress)
178+
document.body.classList.add('config--tasks-progress')
179+
else
180+
document.body.classList.remove('config--tasks-progress')
181+
}
153182
}
154183

155184
function importJson(importEv) {

assets/styles.css

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
--danger: #D54F4F;
2727
--link-color: var(--yellow);
2828

29-
--bottomnav-height: 3.5rem;
29+
--bottomnav-height: 4.5rem;
3030
--bg-navbar: #404040;
3131

3232
--card-padding: 0.75rem;
@@ -312,9 +312,14 @@ task-control {
312312
margin: -1.75rem auto 0;
313313

314314
background-image: url(sticky-tape.png);
315+
background-repeat: no-repeat;
315316
width: 8rem;
316317
height: 2rem;
317318
content: '';
319+
transform: rotate(-4deg);
320+
}
321+
.task-category:nth-child(even)::before {
322+
transform: rotate(4deg);
318323
}
319324
task-category:not(:first-child),
320325
.task-category:not(:first-child) {
@@ -326,6 +331,10 @@ task-category:not(:first-child),
326331
padding: 0.75rem;
327332
}
328333

334+
.task-category .progress-container {
335+
display: none;
336+
}
337+
329338
.task-list .task-list--title,
330339
.task-category .task-category--title {
331340
font-size: 1.2rem;
@@ -398,6 +407,10 @@ task-item .delete,
398407
flex-grow: 1;
399408
}
400409

410+
fieldset {
411+
border-radius: 2px;
412+
}
413+
401414
.field {
402415
margin-bottom: 0.75rem;
403416
}
@@ -583,11 +596,25 @@ input#import_json {
583596
/***** width *****/
584597
.max-w-10r { max-width: 10rem; }
585598
.container { margin: 0 auto; max-width: 90vw; }
599+
.w-full { width: 100%; }
586600

587601
.block { display: block; }
588602
.inline-block { display: inline-block; }
589603
.white-space-none { white-space: none; }
590604

605+
/***** settings/config *****/
606+
607+
body.config--nav-compact {
608+
--bottomnav-height: 3.5rem;
609+
}
610+
611+
body.config--tasks-progress task-category .progress-container {
612+
display: flex;
613+
align-items: align-center;
614+
}
615+
616+
/***** fonts *****/
617+
591618
@font-face {
592619
font-family: 'xkcd';
593620
src: url('fonts/xkcd.otf');

0 commit comments

Comments
 (0)