Skip to content

Commit db0eceb

Browse files
Copilotcubapthehabes
authored
Replace native alert()/confirm() with tpen-alert/tpen-confirm modal dialogs (#496)
* Initial plan * Replace alert() and confirm() with tpen-alert/tpen-confirm modal dialogs Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> * Stop page bump when scroll bar is showing * Fix race condition, accessibility, stacking, and button text in confirm dialogs Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> * Add confirmAction helper and update confirms Introduce confirmAction in api/events.js to centralize confirm dialog handling with unique confirmId to avoid race conditions. Revamp ConfirmContainer to queue dialogs, handle keyboard navigation, manage focus, and dispatch confirm events with confirmId. Improve AlertContainer with keyboard handling and focus management. Replace many ad-hoc TPEN.eventDispatcher one/off/dispatch confirm patterns across components (annotorious-annotator, plain, decline-project, leave-project, manage-layers, manage-pages, manifest-import, navigation-manager, quicktype-manager, roles-handler, manage-project interface) to use confirmAction, simplifying code and reducing listener collisions. Also adjust redirect behavior after alerts in manage-project to wait for acknowledgement. * Update events.js * Replace modal divs with <dialog> and improve modals Convert various modal/overlay containers to use the native <dialog> element and its showModal()/close() API. Update Alert and Confirm components to call showModal when showing and to toggle classes safely. Replace section/div overlays with dialog elements in AlertContainer, ConfirmContainer, ProjectTools, QuickTypeEditorDialog, and UpdateMetadata; add backdrop styles, sizing, and [open] display rules. Add cancel and backdrop click handlers to close dialogs (and prevent default cancel behavior). Ensure screen-locking sections are closed/cleaned up after dialogs dismiss (including retry logic in AlertContainer and delayed close in ConfirmContainer). Misc CSS tweaks to support full-viewport dialogs and backdrop behavior. * Add modalHost utility for less repetition and coupling Introduce utilities/modalHost.js (openModalHost, closeModalHost, closeModalHostWhenEmpty) to centralize dialog host open/close logic and timing retries. Replace scattered showModal/close logic in Alert.js and Confirm.js to use openModalHost; update AlertContainer.js and ConfirmContainer.js to use closeModalHostWhenEmpty for coordinated closing after animations. Add addCustomAlert and a shared keyboard handler in AlertContainer, and simplify transcription code to call addCustomAlert directly. Files changed: components/gui/alert/Alert.js, components/gui/alert/AlertContainer.js, components/gui/confirm/Confirm.js, components/gui/confirm/ConfirmContainer.js, interfaces/transcription/index.js, utilities/modalHost.js. * deduping CSS * TPEN colors first * Queue alerts and refine modal/confirm UX Introduce alert queuing and improve modal/confirm behavior and styling. Alerts are now queued (only one shown at a time) with addAlert/addCustomAlert pushing entries to a queue; show/dismiss logic uses a #showCurrent/dismissCurrent flow, native dialog cancel events for Escape, and Enter handling for activation. Confirm dialogs now use role="alertdialog", default focus is set to the negative/cancel button, keyboard navigation is improved (Tab/Shift+Tab cycling, Enter activation through shadow DOM), and positive/negative handlers validate confirmId against the active dialog. Both containers now append their singleton to document.body, and cancel handlers are removed when dialogs are dismissed to avoid leaks. Shared modal CSS was updated (transparent backdrop, color variables, selector changes to alertdialog, button layout and responsive tweaks). Also fixed an import in line-parser to use the utilities confirmAction path. * Remove dead handler, the condition is always false. Let <button> do its native thing. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cubap <1119165+cubap@users.noreply.github.com> Co-authored-by: Bryan Haberberger <bryan.j.haberberger@slu.edu> Co-authored-by: cubap <cubap@slu.edu>
1 parent c140227 commit db0eceb

File tree

27 files changed

+954
-478
lines changed

27 files changed

+954
-478
lines changed

api/Project.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export default class Project {
258258
return response
259259
} catch (error) {
260260
console.error("Error updating roles:", error)
261-
alert("Failed to update roles. Please try again.")
261+
eventDispatcher.dispatch('tpen-alert', { message: "Failed to update roles. Please try again." })
262262
}
263263
}
264264

components/annotorious-annotator/line-parser.js

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
2020
import { onProjectReady } from '../../utilities/projectReady.js'
2121
import vault from '../../js/vault.js'
2222
import '../page-selector/index.js'
23+
import { confirmAction } from '../../utilities/confirmAction.js'
2324

2425
class AnnotoriousAnnotator extends HTMLElement {
2526
#osd
@@ -466,7 +467,7 @@ class AnnotoriousAnnotator extends HTMLElement {
466467
}, 500)
467468
this.#pendingTimeouts.add(timeoutId)
468469
})
469-
this.renderCleanup.onElement(deleteAllBtn, "click", async (e) => await this.deleteAllAnnotations(e))
470+
this.renderCleanup.onElement(deleteAllBtn, "click", (e) => this.deleteAllAnnotations(e))
470471
this.renderCleanup.onWindow('beforeunload', (ev) => {
471472
if (this.#resolvedAnnotationPage?.$isDirty) {
472473
ev.preventDefault()
@@ -715,8 +716,14 @@ class AnnotoriousAnnotator extends HTMLElement {
715716
srcDown: "../interfaces/annotator/images/transcribe.png",
716717
onClick: (e) => {
717718
if (this.#resolvedAnnotationPage?.$isDirty) {
718-
if (confirm("Stop identifying lines and go transcribe? Unsaved changes will be lost."))
719-
location.href = `/transcribe?projectID=${TPEN.activeProject._id}&pageID=${this.#annotationPageID}`
719+
confirmAction(
720+
"Stop identifying lines and go transcribe? Unsaved changes will be lost.",
721+
() => {
722+
location.href = `/transcribe?projectID=${TPEN.activeProject._id}&pageID=${this.#annotationPageID}`
723+
},
724+
null,
725+
{ positiveButtonText: "Go Transcribe", negativeButtonText: "Keep Editing" }
726+
)
720727
}
721728
else {
722729
location.href = `/transcribe?projectID=${TPEN.activeProject._id}&pageID=${this.#annotationPageID}`
@@ -808,13 +815,15 @@ class AnnotoriousAnnotator extends HTMLElement {
808815
_this.#pendingTimeouts.delete(timeoutId)
809816
// Timeout required in order to allow the click-and-focus native functionality to complete.
810817
// Also stops the goofy UX for naturally slow clickers.
811-
let c = confirm("Are you sure you want to remove this?")
812-
if (c) {
813-
_this.#annotoriousInstance.removeAnnotation(originalAnnotation)
814-
_this.#resolvedAnnotationPage.$isDirty = true
815-
} else {
816-
_this.#annotoriousInstance.cancelSelected()
817-
}
818+
confirmAction(
819+
"Are you sure you want to remove this?",
820+
() => {
821+
_this.#annotoriousInstance.removeAnnotation(originalAnnotation)
822+
_this.#resolvedAnnotationPage.$isDirty = true
823+
},
824+
() => _this.#annotoriousInstance.cancelSelected(),
825+
{ positiveButtonText: "Delete", negativeButtonText: "Cancel" }
826+
)
818827
}, 500)
819828
_this.#pendingTimeouts.add(timeoutId)
820829
}
@@ -1139,30 +1148,37 @@ class AnnotoriousAnnotator extends HTMLElement {
11391148
return this.#modifiedAnnotationPage
11401149
}
11411150

1151+
11421152
/**
11431153
* Use Annotorious to delete all known Annotations
11441154
* https://annotorious.dev/api-reference/openseadragon-annotator/#clearannotations
11451155
*/
1146-
async deleteAllAnnotations() {
1147-
if (!confirm('This will remove all Annotations and will take effect immediately. This action cannot be undone.')) return
1148-
const deleteAllBtn = this.shadowRoot.getElementById("deleteAllBtn")
1149-
deleteAllBtn.setAttribute("disabled", "true")
1150-
deleteAllBtn.textContent = "deleting. please wait..."
1151-
this.#annotoriousInstance.clearAnnotations()
1152-
this.#resolvedAnnotationPage.$isDirty = true
1153-
try {
1154-
await this.saveAnnotations()
1155-
await this.clearColumnsServerSide()
1156-
} catch (err) {
1157-
console.error("Could not delete all annotations.", err)
1158-
TPEN.eventDispatcher.dispatch("tpen-toast", {
1159-
message: "Could not delete all annotations.",
1160-
status: "error"
1161-
})
1162-
} finally {
1163-
deleteAllBtn.removeAttribute("disabled")
1164-
deleteAllBtn.textContent = "Delete All Annotations"
1165-
}
1156+
deleteAllAnnotations() {
1157+
confirmAction(
1158+
"This will remove all Annotations and will take effect immediately. This action cannot be undone.",
1159+
() => {
1160+
const deleteAllBtn = this.shadowRoot.getElementById("deleteAllBtn")
1161+
deleteAllBtn.setAttribute("disabled", "true")
1162+
deleteAllBtn.textContent = "deleting. please wait..."
1163+
this.#annotoriousInstance.clearAnnotations()
1164+
this.#resolvedAnnotationPage.$isDirty = true
1165+
this.saveAnnotations()
1166+
.then(() => this.clearColumnsServerSide())
1167+
.catch(err => {
1168+
console.error("Could not delete all annotations.", err)
1169+
TPEN.eventDispatcher.dispatch("tpen-toast", {
1170+
message: "Could not delete all annotations.",
1171+
status: "error"
1172+
})
1173+
})
1174+
.finally(() => {
1175+
deleteAllBtn.removeAttribute("disabled")
1176+
deleteAllBtn.textContent = "Delete All Annotations"
1177+
})
1178+
},
1179+
null,
1180+
{ positiveButtonText: "Delete All", negativeButtonText: "Cancel" }
1181+
)
11661182
}
11671183

11681184
/**

components/annotorious-annotator/plain.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import TPEN from '../../api/TPEN.js'
1515
import User from '../../api/User.js'
1616
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
17+
import { confirmAction } from '../../utilities/confirmAction.js'
1718

1819
class AnnotoriousAnnotator extends HTMLElement {
1920
#osd
@@ -126,7 +127,7 @@ class AnnotoriousAnnotator extends HTMLElement {
126127
}
127128
this.#annotationPageURI = TPEN.screen.pageInQuery
128129
if(!this.#annotationPageURI) {
129-
alert("You must provide a ?pageID=theid in the URL. The value should be the URI of an existing AnnotationPage.")
130+
TPEN.eventDispatcher.dispatch('tpen-alert', { message: "You must provide a ?pageID=theid in the URL. The value should be the URI of an existing AnnotationPage." })
130131
return
131132
}
132133
this.setAttribute("annotationpage", this.#annotationPageURI)
@@ -257,13 +258,12 @@ class AnnotoriousAnnotator extends HTMLElement {
257258
_this.#eraseConfirmTimeout = null
258259
// Timeout required in order to allow the click-and-focus native functionality to complete.
259260
// Also stops the goofy UX for naturally slow clickers.
260-
let c = confirm("Are you sure you want to remove this?")
261-
if(c) {
262-
_this.#annotoriousInstance.removeAnnotation(annotation)
263-
}
264-
else{
265-
_this.#annotoriousInstance.cancelSelected()
266-
}
261+
confirmAction(
262+
"Are you sure you want to remove this?",
263+
() => _this.#annotoriousInstance.removeAnnotation(annotation),
264+
() => _this.#annotoriousInstance.cancelSelected(),
265+
{ positiveButtonText: "Delete", negativeButtonText: "Cancel" }
266+
)
267267
}, 500)
268268
}
269269
})

components/decline-project/index.js

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import TPEN from "../../api/TPEN.js"
22
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
3+
import { confirmAction } from '../../utilities/confirmAction.js'
34

45
/**
56
* DeclineInvite - Allows invited users to decline a project invitation.
@@ -87,45 +88,51 @@ class DeclineInvite extends HTMLElement {
8788
}
8889

8990
declineInvitation(collaboratorID, projectID) {
90-
if (!confirm("You are declining a chance to be a part of this TPEN3 project.")) return
91-
let redir = true
92-
const declineBtn = this.shadowRoot.getElementById("declineBtn")
93-
declineBtn.setAttribute("disabled", "disabled")
94-
declineBtn.setAttribute("value", "declining...")
95-
fetch(`${TPEN.servicesURL}/project/${this.#project}/collaborator/${this.#user}/decline`)
96-
.then(resp => {
97-
if (resp.ok) return resp.text()
98-
redir = false
99-
return resp.json()
100-
})
101-
.then(message => {
102-
let userMessage = (typeof message === "string") ? message : message?.message
103-
if (redir) {
104-
this.shadowRoot.innerHTML = `
105-
<h3> ${userMessage} </h3>
106-
`
107-
setTimeout(() => {
108-
document.location.href = TPEN.TPEN3URL
109-
}, 3000)
110-
return
111-
}
112-
this.shadowRoot.innerHTML = `
113-
<h3>There was an error declining the invitation.</h3>
114-
<p>
115-
The message below has more details.
116-
Refresh the page to try again or contact the TPEN3 Administrators.
117-
</p>
118-
<code> ${userMessage} <code>
119-
`
120-
})
121-
.catch(err => {
122-
this.shadowRoot.innerHTML = `
123-
<h3>
124-
There was an error declining the invitation. Refresh the page to try again
125-
or contact the TPEN3 Administrators.
126-
</h3>
127-
`
128-
})
91+
confirmAction(
92+
"You are declining a chance to be a part of this TPEN3 project.",
93+
() => {
94+
let redir = true
95+
const declineBtn = this.shadowRoot.getElementById("declineBtn")
96+
declineBtn.setAttribute("disabled", "disabled")
97+
declineBtn.setAttribute("value", "declining...")
98+
fetch(`${TPEN.servicesURL}/project/${this.#project}/collaborator/${this.#user}/decline`)
99+
.then(resp => {
100+
if (resp.ok) return resp.text()
101+
redir = false
102+
return resp.json()
103+
})
104+
.then(message => {
105+
let userMessage = (typeof message === "string") ? message : message?.message
106+
if (redir) {
107+
this.shadowRoot.innerHTML = `
108+
<h3> ${userMessage} </h3>
109+
`
110+
setTimeout(() => {
111+
document.location.href = TPEN.TPEN3URL
112+
}, 3000)
113+
return
114+
}
115+
this.shadowRoot.innerHTML = `
116+
<h3>There was an error declining the invitation.</h3>
117+
<p>
118+
The message below has more details.
119+
Refresh the page to try again or contact the TPEN3 Administrators.
120+
</p>
121+
<code> ${userMessage} <code>
122+
`
123+
})
124+
.catch(err => {
125+
this.shadowRoot.innerHTML = `
126+
<h3>
127+
There was an error declining the invitation. Refresh the page to try again
128+
or contact the TPEN3 Administrators.
129+
</h3>
130+
`
131+
})
132+
},
133+
null,
134+
{ positiveButtonText: "Decline", negativeButtonText: "Cancel" }
135+
)
129136
}
130137
}
131138

components/gui/alert/Alert.js

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

45
/**
56
* Alert - A modal alert dialog that requires user acknowledgement.
@@ -25,11 +26,11 @@ class Alert extends HTMLElement {
2526
* Have them appear with a dropdown effect.
2627
*/
2728
show() {
28-
this.closest(".alert-area").style.display = "grid"
29+
const alertArea = this.closest('.alert-area')
30+
openModalHost(alertArea)
2931
const showTimer = setTimeout(() => {
30-
this.closest(".alert-area").classList.add("show")
32+
alertArea?.classList.add('show')
3133
this.classList.add('show')
32-
document.querySelector("body").style.overflow = "hidden"
3334
}, 1)
3435
this.cleanup.add(() => clearTimeout(showTimer))
3536
eventDispatcher.dispatch("tpen-alert-activated")
@@ -41,8 +42,7 @@ class Alert extends HTMLElement {
4142
*/
4243
dismiss() {
4344
this.classList.remove('show')
44-
this.closest(".alert-area")?.classList.remove("show")
45-
document.querySelector("body").style.overflow = "auto"
45+
this.closest('.alert-area')?.classList.remove('show')
4646
const removeTimer = setTimeout(() => {
4747
this.remove()
4848
}, 500)

0 commit comments

Comments
 (0)