Skip to content

Commit e62e617

Browse files
committed
Disable export buttons on form submit
1 parent 6a5e371 commit e62e617

File tree

3 files changed

+25
-142
lines changed

3 files changed

+25
-142
lines changed

openlibrary/plugins/openlibrary/js/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,11 @@ jQuery(function () {
228228
.then((module) => new module.LazyThingPreview().init());
229229
}
230230

231-
// Enhance patron export buttons on /account/import
232-
if (location.pathname === '/account/import') {
231+
// Disable data export buttons on form submit
232+
const patronImportForms = document.querySelectorAll('.patron-export-form')
233+
if (patronImportForms.length) {
233234
import(/* webpackChunkName: "patron-exports" */ './patron_exports')
234-
.then(module => module.initPatronExportButtons());
235+
.then(module => module.initPatronExportForms(patronImportForms));
235236
}
236237

237238
const $observationModalLinks = $('.observations-modal-link');
Lines changed: 16 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,26 @@
11
/**
2-
* Enhances export buttons on /account/import so that when an export starts:
3-
* - The clicked button shows "Downloading..."
4-
* - The button is disabled and styled as unavailable (grey)
5-
* - Subsequent clicks are prevented while the download is in progress
2+
* Switches the given button to a disabled state.
63
*
7-
* Implementation detail: We use fetch() to retrieve the CSV as a Blob so we
8-
* can reliably detect completion across browsers (some do not fire iframe
9-
* load events when Content-Disposition: attachment is used). Once the blob is
10-
* received, we trigger a programmatic download and then restore button state.
4+
* @param {HTMLElement} buttonElement
115
*/
12-
13-
function getSubmitButton(formElement) {
14-
// Each export form has a single submit input
15-
return formElement.querySelector('input[type="submit"], button[type="submit"]');
16-
}
17-
186
function disableButton(buttonElement) {
19-
if (!buttonElement) return;
20-
buttonElement.dataset.originalValue = buttonElement.value || buttonElement.textContent || '';
21-
if (buttonElement.tagName === 'INPUT') {
22-
buttonElement.value = 'Downloading...';
23-
} else {
24-
buttonElement.textContent = 'Downloading...';
25-
}
267
buttonElement.setAttribute('disabled', 'true');
278
buttonElement.setAttribute('aria-disabled', 'true');
28-
buttonElement.classList.remove('cta-btn--available');
29-
buttonElement.classList.add('cta-btn--unavailable');
30-
// Show loading affordance consistent with other unavailable actions
31-
buttonElement.classList.add('cta-btn--unavailable--load');
32-
}
33-
34-
function enableButton(buttonElement) {
35-
if (!buttonElement) return;
36-
const original = buttonElement.dataset.originalValue || '';
37-
if (buttonElement.tagName === 'INPUT') {
38-
buttonElement.value = original;
39-
} else {
40-
buttonElement.textContent = original;
41-
}
42-
buttonElement.removeAttribute('disabled');
43-
buttonElement.setAttribute('aria-disabled', 'false');
44-
buttonElement.classList.remove('cta-btn--unavailable');
45-
buttonElement.classList.remove('cta-btn--unavailable--load');
46-
buttonElement.classList.add('cta-btn--available');
47-
delete buttonElement.dataset.originalValue;
48-
}
49-
50-
function buildGetUrlFromForm(formElement) {
51-
const action = formElement.getAttribute('action') || '';
52-
const params = new URLSearchParams(new FormData(formElement));
53-
const query = params.toString();
54-
return query ? `${action}?${query}` : action;
55-
}
56-
57-
function parseFilenameFromContentDisposition(headerValue) {
58-
if (!headerValue) return null;
59-
// RFC 5987 filename*=
60-
const starMatch = /filename\*=UTF-8''([^;]+)/i.exec(headerValue);
61-
if (starMatch && starMatch[1]) {
62-
try {
63-
return decodeURIComponent(starMatch[1]);
64-
} catch (_) {
65-
return starMatch[1];
66-
}
67-
}
68-
// Basic filename="..."
69-
const plainMatch = /filename="?([^";]+)"?/i.exec(headerValue);
70-
return plainMatch && plainMatch[1] ? plainMatch[1] : null;
719
}
7210

73-
async function fetchAndDownload(url) {
74-
const response = await fetch(url, { credentials: 'same-origin' });
75-
if (!response.ok) {
76-
const error = new Error(`Export request failed with status ${response.status}`);
77-
error.status = response.status;
78-
throw error;
79-
}
80-
const blob = await response.blob();
81-
const contentDisposition = response.headers.get('Content-Disposition') || response.headers.get('content-disposition');
82-
const fallbackName = 'OpenLibrary_Export.csv';
83-
const filename = parseFilenameFromContentDisposition(contentDisposition) || fallbackName;
84-
const objectUrl = URL.createObjectURL(blob);
85-
const a = document.createElement('a');
86-
a.href = objectUrl;
87-
a.download = filename;
88-
document.body.appendChild(a);
89-
a.click();
90-
setTimeout(() => {
91-
URL.revokeObjectURL(objectUrl);
92-
if (a.parentNode) a.parentNode.removeChild(a);
93-
}, 0);
94-
}
95-
96-
export function initPatronExportButtons() {
97-
// Guard: only on the import page
98-
if (location.pathname !== '/account/import') return;
99-
100-
const exportForms = document.querySelectorAll('form[action="/account/export"][method="GET" i]');
101-
if (!exportForms.length) return;
102-
103-
exportForms.forEach((form) => {
104-
const submitButton = getSubmitButton(form);
105-
if (!submitButton) return;
106-
107-
// Prevent double-binding
108-
if (form.dataset.patronExportBound === 'true') return;
109-
form.dataset.patronExportBound = 'true';
110-
111-
form.addEventListener('submit', (event) => {
112-
// If already disabled, block to prevent duplicates
113-
if (submitButton.hasAttribute('disabled')) {
114-
event.preventDefault();
115-
return;
116-
}
117-
118-
event.preventDefault();
11+
/**
12+
* Adds `submit` listeners for the given form elements.
13+
*
14+
* When any of the given forms are submitted, the form's
15+
* submit button is disabled.
16+
*
17+
* @param {NodeList<HTMLFormElement>} elems
18+
*/
19+
export function initPatronExportForms(elems) {
20+
elems.forEach((form) => {
21+
const submitButton = form.querySelector('input[type=submit]')
22+
form.addEventListener("submit", () => {
11923
disableButton(submitButton);
120-
const startMs = performance.now();
121-
const url = buildGetUrlFromForm(form);
122-
fetchAndDownload(url)
123-
.catch(() => {
124-
// Swallow errors but restore button;
125-
})
126-
.finally(() => {
127-
// Ensure state stays visible for at least a short time
128-
const minDurationMs = 500;
129-
const elapsed = performance.now() - startMs;
130-
const remaining = Math.max(0, minDurationMs - elapsed);
131-
setTimeout(() => enableButton(submitButton), remaining);
132-
});
133-
});
134-
135-
// Also handle direct button clicks that might bypass submit event ordering
136-
submitButton.addEventListener('click', (event) => {
137-
if (submitButton.hasAttribute('disabled')) {
138-
event.preventDefault();
139-
}
140-
});
141-
});
24+
})
25+
})
14226
}
143-
144-

openlibrary/templates/account/import.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ <h3>$_("Import from Goodreads")</h3>
2323
<h3>$_("Export your Reading Log")</h3>
2424
<p>$_("Download a copy of your reading log.") <a href="/help/faq/reading-log.en#reading-log">$_("What is a reading log?")</a></p>
2525
<form method="GET" action="/account/export"
26-
enctype="multipart/form-data" class="olform olform--decoration">
26+
enctype="multipart/form-data" class="olform olform--decoration patron-export-form">
2727
<input type="hidden" name="type" value="reading_log">
2828
<div>
2929
<input type="submit" class="cta-btn cta-btn--available cta-btn--small" value="$_('Download (.csv format)')" data-ol-link-track="PatronExports|ReadingLog">
@@ -34,7 +34,7 @@ <h3>$_("Export your Reading Log")</h3>
3434
<h3>$_("Export your book notes")</h3>
3535
<p>$_("Download a copy of your book notes.") <a href="/help/faq/book-notes">$_("What are book notes?")</a></p>
3636
<form method="GET" action="/account/export"
37-
enctype="multipart/form-data" class="olform olform--decoration">
37+
enctype="multipart/form-data" class="olform olform--decoration patron-export-form">
3838
<input type="hidden" name="type" value="book_notes">
3939
<div>
4040
<input type="submit" class="cta-btn cta-btn--available cta-btn--small" value="$_('Download (.csv format)')" data-ol-link-track="PatronExports|BookNotes">
@@ -45,7 +45,7 @@ <h3>$_("Export your book notes")</h3>
4545
<h3>$_("Export your reviews")</h3>
4646
<p>$_("Download a copy of your review tags.") <a href="/help/faq/reviews#book-tags">$_("What are review tags?")</a></p>
4747
<form method="GET" action="/account/export"
48-
enctype="multipart/form-data" class="olform olform--decoration">
48+
enctype="multipart/form-data" class="olform olform--decoration patron-export-form">
4949
<input type="hidden" name="type" value="reviews">
5050
<div>
5151
<input type="submit" class="cta-btn cta-btn--available cta-btn--small" value="$_('Download (.csv format)')" data-ol-link-track="PatronExports|ReviewTags">
@@ -56,7 +56,7 @@ <h3>$_("Export your reviews")</h3>
5656
<h3>$_("Export your list overview")</h3>
5757
<p>$_("Download a summary of your lists and their contents.") <a href="/help/faq/reading-log.en#lists">$_("What are lists?")</a></p>
5858
<form method="GET" action="/account/export"
59-
enctype="multipart/form-data" class="olform olform--decoration">
59+
enctype="multipart/form-data" class="olform olform--decoration patron-export-form">
6060
<input type="hidden" name="type" value="lists">
6161
<div>
6262
<input type="submit" class="cta-btn cta-btn--available cta-btn--small" value="$_('Download (.csv format)')" data-ol-link-track="PatronExports|ListsSummary">
@@ -67,7 +67,7 @@ <h3>$_("Export your list overview")</h3>
6767
<h3>$_("Export your star ratings")</h3>
6868
<p>$_("Download a copy of your star ratings.") <a href="/help/faq/reviews#star-ratings">$_("What are star ratings?")</a></p>
6969
<form method="GET" action="/account/export"
70-
enctype="multipart/form-data" class="olform olform--decoration">
70+
enctype="multipart/form-data" class="olform olform--decoration patron-export-form">
7171
<input type="hidden" name="type" value="ratings">
7272
<div>
7373
<input type="submit" class="cta-btn cta-btn--available cta-btn--small" value="$_('Download (.csv format)')" data-ol-link-track="PatronExports|StarRatings">

0 commit comments

Comments
 (0)