Skip to content

Commit 5d4c16e

Browse files
committed
more passes at a consistent web component lifecycle
1 parent 1f2af88 commit 5d4c16e

11 files changed

Lines changed: 164 additions & 135 deletions

File tree

components/gui/alert/Alert.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { eventDispatcher } from '../../../api/events.js'
22

3+
/**
4+
* Alert - A modal alert dialog that requires user acknowledgement.
5+
* Takes over the screen until dismissed.
6+
* @element tpen-alert
7+
*/
38
class Alert extends HTMLElement {
9+
/** @type {number|null} Timer ID for show animation */
10+
_showTimer = null
11+
/** @type {number|null} Timer ID for removal animation */
12+
_removeTimer = null
13+
414
constructor() {
515
super()
616
this.attachShadow({ mode: 'open' })
@@ -17,7 +27,7 @@ class Alert extends HTMLElement {
1727
*/
1828
show() {
1929
this.closest(".alert-area").style.display = "grid"
20-
setTimeout(() => {
30+
this._showTimer = setTimeout(() => {
2131
this.closest(".alert-area").classList.add("show")
2232
this.classList.add('show')
2333
document.querySelector("body").style.overflow = "hidden"
@@ -33,12 +43,16 @@ class Alert extends HTMLElement {
3343
this.classList.remove('show')
3444
this.closest(".alert-area").classList.remove("show")
3545
document.querySelector("body").style.overflow = "auto"
36-
setTimeout(() => {
46+
this._removeTimer = setTimeout(() => {
3747
this.remove()
3848
}, 500)
3949
eventDispatcher.dispatch("tpen-alert-acknowledged")
4050
}
4151

52+
disconnectedCallback() {
53+
if (this._showTimer) clearTimeout(this._showTimer)
54+
if (this._removeTimer) clearTimeout(this._removeTimer)
55+
}
4256
}
4357

4458
customElements.define('tpen-alert', Alert)

components/gui/alert/AlertContainer.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import './Alert.js'
22
import { eventDispatcher } from '../../../api/events.js'
3+
import { CleanupRegistry } from '../../../utilities/CleanupRegistry.js'
34

45
/**
56
* AlertContainer - Global container for displaying alert dialogs.
@@ -8,8 +9,8 @@ import { eventDispatcher } from '../../../api/events.js'
89
*/
910
class AlertContainer extends HTMLElement {
1011
#screenLockingSection
11-
/** @type {Function|null} Handler for alert events */
12-
_alertHandler = null
12+
/** @type {CleanupRegistry} Registry for cleanup handlers */
13+
cleanup = new CleanupRegistry()
1314

1415
constructor() {
1516
super()
@@ -18,14 +19,12 @@ class AlertContainer extends HTMLElement {
1819
}
1920

2021
connectedCallback() {
21-
this._alertHandler = ({ detail }) => this.addAlert(detail?.message, detail?.buttonText)
22-
eventDispatcher.on('tpen-alert', this._alertHandler)
22+
const alertHandler = ({ detail }) => this.addAlert(detail?.message, detail?.buttonText)
23+
this.cleanup.onEvent(eventDispatcher, 'tpen-alert', alertHandler)
2324
}
2425

2526
disconnectedCallback() {
26-
if (this._alertHandler) {
27-
eventDispatcher.off('tpen-alert', this._alertHandler)
28-
}
27+
this.cleanup.run()
2928
}
3029

3130
/**

components/gui/confirm/Confirm.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { eventDispatcher } from '../../../api/events.js'
22

3+
/**
4+
* Confirm - A modal confirmation dialog with positive/negative options.
5+
* Takes over the screen until user makes a choice.
6+
* @element tpen-confirm
7+
*/
38
class Confirm extends HTMLElement {
9+
/** @type {number|null} Timer ID for show animation */
10+
_showTimer = null
11+
/** @type {number|null} Timer ID for removal animation */
12+
_removeTimer = null
13+
414
constructor() {
515
super()
616
this.attachShadow({ mode: 'open' })
@@ -17,7 +27,7 @@ class Confirm extends HTMLElement {
1727
*/
1828
show() {
1929
this.closest(".confirm-area").style.display = "grid"
20-
setTimeout(() => {
30+
this._showTimer = setTimeout(() => {
2131
this.closest(".confirm-area").classList.add("show")
2232
this.classList.add('show')
2333
document.querySelector("body").style.overflow = "hidden"
@@ -33,11 +43,15 @@ class Confirm extends HTMLElement {
3343
this.classList.remove('show')
3444
this.closest(".confirm-area").classList.remove("show")
3545
document.querySelector("body").style.overflow = "auto"
36-
setTimeout(() => {
46+
this._removeTimer = setTimeout(() => {
3747
this.remove()
3848
}, 500)
3949
}
4050

51+
disconnectedCallback() {
52+
if (this._showTimer) clearTimeout(this._showTimer)
53+
if (this._removeTimer) clearTimeout(this._removeTimer)
54+
}
4155
}
4256

4357
customElements.define('tpen-confirm', Confirm)

components/gui/confirm/ConfirmContainer.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import './Confirm.js'
22
import { eventDispatcher } from '../../../api/events.js'
3+
import { CleanupRegistry } from '../../../utilities/CleanupRegistry.js'
34

45
/**
56
* ConfirmContainer - Global container for displaying confirmation dialogs.
@@ -9,12 +10,8 @@ import { eventDispatcher } from '../../../api/events.js'
910
class ConfirmContainer extends HTMLElement {
1011
#screenLockingSection
1112
#confirmElem
12-
/** @type {Function|null} Handler for confirm events */
13-
_confirmHandler = null
14-
/** @type {Function|null} Handler for positive button events */
15-
_positiveHandler = null
16-
/** @type {Function|null} Handler for negative button events */
17-
_negativeHandler = null
13+
/** @type {CleanupRegistry} Registry for cleanup handlers */
14+
cleanup = new CleanupRegistry()
1815

1916
constructor() {
2017
super()
@@ -23,19 +20,17 @@ class ConfirmContainer extends HTMLElement {
2320
}
2421

2522
connectedCallback() {
26-
this._confirmHandler = ({ detail }) => this.addConfirm(detail?.message, detail?.positiveButtonText, detail.negativeButtonText)
27-
this._positiveHandler = () => this.#confirmElem?.dismiss()
28-
this._negativeHandler = () => this.#confirmElem?.dismiss()
23+
const confirmHandler = ({ detail }) => this.addConfirm(detail?.message, detail?.positiveButtonText, detail.negativeButtonText)
24+
const positiveHandler = () => this.#confirmElem?.dismiss()
25+
const negativeHandler = () => this.#confirmElem?.dismiss()
2926

30-
eventDispatcher.on('tpen-confirm', this._confirmHandler)
31-
eventDispatcher.on('tpen-confirm-positive', this._positiveHandler)
32-
eventDispatcher.on('tpen-confirm-negative', this._negativeHandler)
27+
this.cleanup.onEvent(eventDispatcher, 'tpen-confirm', confirmHandler)
28+
this.cleanup.onEvent(eventDispatcher, 'tpen-confirm-positive', positiveHandler)
29+
this.cleanup.onEvent(eventDispatcher, 'tpen-confirm-negative', negativeHandler)
3330
}
3431

3532
disconnectedCallback() {
36-
if (this._confirmHandler) eventDispatcher.off('tpen-confirm', this._confirmHandler)
37-
if (this._positiveHandler) eventDispatcher.off('tpen-confirm-positive', this._positiveHandler)
38-
if (this._negativeHandler) eventDispatcher.off('tpen-confirm-negative', this._negativeHandler)
33+
this.cleanup.run()
3934
}
4035

4136
/**

components/gui/site/Header.js

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import TPEN from "../../../api/TPEN.js"
2+
import { CleanupRegistry } from '../../../utilities/CleanupRegistry.js'
23

34
/**
45
* TpenHeader - Main site header with navigation, title, and action button.
56
* @element tpen-header
67
*/
78
class TpenHeader extends HTMLElement {
9+
/** @type {CleanupRegistry} Registry for cleanup handlers */
10+
cleanup = new CleanupRegistry()
811
/** @type {Function|null} Handler for title events */
912
_titleHandler = null
1013
/** @type {Function|null} Handler for action link events */
1114
_actionLinkHandler = null
1215
/** @type {Function|null} Handler for action link remove events */
1316
_actionLinkRemoveHandler = null
17+
/** @type {Function|null} Handler for logout button */
18+
_logoutHandler = null
1419

1520
constructor() {
1621
super();
@@ -60,6 +65,8 @@ class TpenHeader extends HTMLElement {
6065
`;
6166
}
6267
connectedCallback() {
68+
TPEN.attachAuthentication(this)
69+
6370
this._titleHandler = ev => {
6471
const title = this.shadowRoot.querySelector('.banner')
6572
if (!ev.detail) {
@@ -82,23 +89,18 @@ class TpenHeader extends HTMLElement {
8289
btn.removeEventListener('click', ev.detail.callback)
8390
}
8491

85-
TPEN.eventDispatcher.on('tpen-gui-title', this._titleHandler)
86-
TPEN.eventDispatcher.on('tpen-gui-action-link', this._actionLinkHandler)
87-
TPEN.eventDispatcher.on('tpen-gui-action-link-remove', this._actionLinkRemoveHandler)
92+
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-gui-title', this._titleHandler)
93+
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-gui-action-link', this._actionLinkHandler)
94+
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-gui-action-link-remove', this._actionLinkRemoveHandler)
8895

8996
this._logoutHandler = () => TPEN.logout()
90-
this.shadowRoot.querySelector('.logout-btn').addEventListener('click', this._logoutHandler)
97+
const logoutBtn = this.shadowRoot.querySelector('.logout-btn')
98+
this.cleanup.onElement(logoutBtn, 'click', this._logoutHandler)
9199
this.setupDraggableButton()
92100
}
93101

94102
disconnectedCallback() {
95-
if (this._titleHandler) TPEN.eventDispatcher.off('tpen-gui-title', this._titleHandler)
96-
if (this._actionLinkHandler) TPEN.eventDispatcher.off('tpen-gui-action-link', this._actionLinkHandler)
97-
if (this._actionLinkRemoveHandler) TPEN.eventDispatcher.off('tpen-gui-action-link-remove', this._actionLinkRemoveHandler)
98-
const logoutBtn = this.shadowRoot?.querySelector('.logout-btn')
99-
if (logoutBtn && this._logoutHandler) {
100-
logoutBtn.removeEventListener('click', this._logoutHandler)
101-
}
103+
this.cleanup.run()
102104
}
103105

104106
setupDraggableButton() {
@@ -126,23 +128,23 @@ class TpenHeader extends HTMLElement {
126128
btn.style.left = '0px'
127129
initialRect = btn.getBoundingClientRect()
128130
}
129-
131+
130132
const header = this.shadowRoot.querySelector('header')
131133
const headerRect = header.getBoundingClientRect()
132134
const btnWidth = initialRect.width
133-
135+
134136
// Calculate bounds relative to initial position
135137
const maxLeft = headerRect.right - initialRect.right - 20 // Space to right edge
136138
const maxRight = headerRect.left - initialRect.left + 20 // Space to left edge
137-
139+
138140
return { maxLeft, maxRight }
139141
}
140142

141143
const animate = () => {
142144
if (Math.abs(velocityX) > MIN_VELOCITY) {
143145
currentX += velocityX
144146
velocityX *= FRICTION
145-
147+
146148
// Check boundaries and bounce
147149
const bounds = getBounds()
148150
if (currentX > bounds.maxLeft) {
@@ -152,7 +154,7 @@ class TpenHeader extends HTMLElement {
152154
currentX = bounds.maxRight
153155
velocityX = Math.abs(velocityX) * BOUNCE_DAMPING // Bounce back with damping
154156
}
155-
157+
156158
btn.style.left = `${currentX}px`
157159
animationFrame = requestAnimationFrame(animate)
158160
} else {
@@ -166,7 +168,7 @@ class TpenHeader extends HTMLElement {
166168
cancelAnimationFrame(animationFrame)
167169
animationFrame = null
168170
}
169-
171+
170172
isDragging = true
171173
hasMoved = false
172174
dragStartTime = Date.now()
@@ -182,24 +184,24 @@ class TpenHeader extends HTMLElement {
182184

183185
const onPointerMove = (e) => {
184186
if (!isDragging) return
185-
187+
186188
const now = Date.now()
187189
const deltaTime = now - lastTime
188190
const deltaX = e.clientX - startX
189-
191+
190192
if (Math.abs(deltaX - currentX) > DRAG_THRESHOLD) {
191193
hasMoved = true
192194
}
193-
195+
194196
// Calculate velocity for momentum
195197
if (deltaTime > 0) {
196198
velocityX = (e.clientX - lastX) / deltaTime * 16 // Normalize to ~60fps
197199
}
198-
200+
199201
// Constrain to viewport bounds while dragging
200202
const bounds = getBounds()
201203
currentX = Math.max(bounds.maxRight, Math.min(bounds.maxLeft, deltaX))
202-
204+
203205
lastX = e.clientX
204206
lastTime = now
205207
btn.style.position = 'relative'
@@ -208,39 +210,48 @@ class TpenHeader extends HTMLElement {
208210

209211
const onPointerUp = (e) => {
210212
if (!isDragging) return
211-
213+
212214
isDragging = false
213215
btn.style.cursor = 'grab'
214216
btn.releasePointerCapture(e.pointerId)
215-
217+
216218
const dragDuration = Date.now() - dragStartTime
217-
219+
218220
// If the button was dragged significantly (distance or time), prevent the click
219221
if (hasMoved || dragDuration > TIME_THRESHOLD) {
220222
e.preventDefault()
221223
e.stopPropagation()
222224
}
223-
225+
224226
// Apply momentum if there's velocity
225227
if (Math.abs(velocityX) > MIN_VELOCITY && hasMoved) {
226228
animationFrame = requestAnimationFrame(animate)
227229
}
228230
}
229231

230232
// Prevent click if drag occurred
231-
btn.addEventListener('click', (e) => {
233+
const clickHandler = (e) => {
232234
if (hasMoved) {
233235
e.preventDefault()
234236
e.stopPropagation()
235237
}
236-
}, true)
238+
}
237239

238-
btn.addEventListener('pointerdown', onPointerDown)
239-
btn.addEventListener('pointermove', onPointerMove)
240-
btn.addEventListener('pointerup', onPointerUp)
241-
btn.addEventListener('pointercancel', onPointerUp)
240+
// Register all button event listeners with cleanup registry
241+
this.cleanup.onElement(btn, 'click', clickHandler, true)
242+
this.cleanup.onElement(btn, 'pointerdown', onPointerDown)
243+
this.cleanup.onElement(btn, 'pointermove', onPointerMove)
244+
this.cleanup.onElement(btn, 'pointerup', onPointerUp)
245+
this.cleanup.onElement(btn, 'pointercancel', onPointerUp)
242246
btn.style.cursor = 'grab'
243247
btn.style.touchAction = 'none'
248+
249+
// Track animation frame for cleanup
250+
this.cleanup.add(() => {
251+
if (animationFrame) {
252+
cancelAnimationFrame(animationFrame)
253+
}
254+
})
244255
}
245256
}
246257

0 commit comments

Comments
 (0)