-
Notifications
You must be signed in to change notification settings - Fork 172
Simple fix for the CfP proposal progress issue #1551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 1 commit
3df7224
8ea953d
420f0e7
a9d6497
39f554f
8f0d4f5
24869d4
949fc09
4cb8aa5
6df7d43
1cc125e
97be255
f0274a4
c335825
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| /** | ||
| * Auto-save CfP form data to sessionStorage to preserve it during browser navigation | ||
| * This ensures data is not lost when users use the browser back button | ||
| */ | ||
|
|
||
| (function() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This style of JS code (wrap the whole code in an anonymous function) is outdated and verbose.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably I'm outdated with respect to js ;-) |
||
| 'use strict'; | ||
|
|
||
| // Get unique storage key based on the current URL (includes tmpid and step) | ||
| function getStorageKey() { | ||
| const path = window.location.pathname; | ||
| return `cfp_form_data_${path}`; | ||
| } | ||
|
|
||
| // Debounce function to avoid saving too frequently | ||
| function debounce(func, wait) { | ||
| let timeout; | ||
| return function executedFunction(...args) { | ||
| const later = () => { | ||
| clearTimeout(timeout); | ||
| func(...args); | ||
| }; | ||
| clearTimeout(timeout); | ||
| timeout = setTimeout(later, wait); | ||
| }; | ||
| } | ||
|
|
||
| // Save form data to sessionStorage | ||
| function saveFormData() { | ||
| const form = document.getElementById('cfp-submission-form'); | ||
| if (!form) return; | ||
|
|
||
| const formData = {}; | ||
|
|
||
| // Find all form fields using querySelectorAll for reliability | ||
| const textareas = form.querySelectorAll('textarea'); | ||
| const inputs = form.querySelectorAll('input:not([type="hidden"]):not([type="submit"])'); | ||
| const selects = form.querySelectorAll('select'); | ||
|
|
||
| // Process all textareas | ||
| textareas.forEach((textarea) => { | ||
| const name = textarea.name; | ||
| if (name && name !== 'csrfmiddlewaretoken') { | ||
| formData[name] = textarea.value; | ||
| } | ||
| }); | ||
|
Comment on lines
+37
to
+46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: Checkbox groups with the same name are not handled correctly in auto-save The logic currently saves a single boolean per checkbox |
||
|
|
||
| // Process all inputs | ||
| inputs.forEach((input) => { | ||
| const name = input.name; | ||
| const type = input.type; | ||
|
|
||
| if (!name || name === 'csrfmiddlewaretoken' || name === 'action') return; | ||
|
|
||
| if (type === 'checkbox') { | ||
|
Comment on lines
+48
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Checkbox groups with the same name are not handled correctly when saving/restoring. The current logic stores a single boolean per checkbox name ( For checkbox groups that represent multi-select fields, store an array of checked values keyed by // saveFormData
if (type === 'checkbox') {
if (!formData[name]) formData[name] = [];
if (input.checked) formData[name].push(input.value);
}// restore
if (elements.length > 1 && elements[0].type === 'checkbox' && Array.isArray(value)) {
for (let i = 0; i < elements.length; i++) {
elements[i].checked = value.includes(elements[i].value);
}
continue;
}This preserves the correct set of checked options per group. |
||
| formData[name] = input.checked; | ||
| } else if (type === 'radio') { | ||
| if (input.checked) { | ||
| formData[name] = input.value; | ||
| } | ||
| } else if (type !== 'file') { | ||
| formData[name] = input.value; | ||
| } | ||
| }); | ||
|
|
||
| // Process all selects | ||
| selects.forEach((select) => { | ||
| const name = select.name; | ||
| if (name && name !== 'csrfmiddlewaretoken') { | ||
| if (select.multiple) { | ||
| formData[name] = Array.from(select.selectedOptions).map(opt => opt.value); | ||
| } else { | ||
| formData[name] = select.value; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| try { | ||
| sessionStorage.setItem(getStorageKey(), JSON.stringify(formData)); | ||
| } catch (e) { | ||
| console.warn('Failed to save form data to sessionStorage:', e); | ||
| } | ||
| } | ||
|
|
||
| // Restore form data from sessionStorage | ||
| function restoreFormData() { | ||
| try { | ||
| const savedData = sessionStorage.getItem(getStorageKey()); | ||
| if (!savedData) return; | ||
|
|
||
| const formData = JSON.parse(savedData); | ||
| const form = document.getElementById('cfp-submission-form'); | ||
| if (!form) return; | ||
|
|
||
| // Check if the saved sessionStorage data matches the current form data | ||
| // If they match exactly, it means we successfully submitted and the server | ||
| // echoed back our data - in this case, clear sessionStorage | ||
| let allFieldsMatch = true; | ||
| let checkedFields = 0; | ||
|
|
||
| for (const [name, savedValue] of Object.entries(formData)) { | ||
| const elements = form.elements[name]; | ||
| if (!elements) continue; | ||
|
|
||
| const element = elements.length !== undefined ? elements[0] : elements; | ||
| const currentValue = element.value || ''; | ||
| const savedValueStr = String(savedValue || ''); | ||
|
Comment on lines
+98
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): The "all fields match" detection for clearing sessionStorage is likely too weak and may never trigger. This comparison only uses To make this reliable, derive Suggested implementation: // Check if the saved sessionStorage data matches the current form data.
// If they match exactly, it means we successfully submitted and the server
// echoed back our data - in this case, clear sessionStorage.
// We reconstruct the current form data in the same shape as what we store.
const currentData = {};
const formDataFromDom = new FormData(form);
for (const [name, value] of formDataFromDom.entries()) {
if (Object.prototype.hasOwnProperty.call(currentData, name)) {
const existing = currentData[name];
if (Array.isArray(existing)) {
existing.push(value);
} else {
currentData[name] = [existing, value];
}
} else {
currentData[name] = value;
}
}
const serializedCurrentData = JSON.stringify(currentData);
// If the serialized current form data matches what we have stored,
// it means the form was successfully submitted - clear sessionStorage.
if (serializedCurrentData === savedData) {
clearFormData();
return;
}To be fully robust and future-proof, you should ensure that
|
||
|
|
||
| checkedFields++; | ||
| if (currentValue.trim() !== savedValueStr.trim()) { | ||
|
Comment on lines
+101
to
+110
|
||
| allFieldsMatch = false; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // If all saved fields match current values AND we checked some fields, | ||
| // it means the form was successfully submitted - clear sessionStorage | ||
| if (allFieldsMatch && checkedFields > 0) { | ||
| clearFormData(); | ||
| return; | ||
| } | ||
|
Comment on lines
+95
to
+121
|
||
|
|
||
| // Otherwise, restore from sessionStorage | ||
| for (const [name, value] of Object.entries(formData)) { | ||
| const elements = form.elements[name]; | ||
| if (!elements) continue; | ||
|
|
||
| // Handle NodeList (radio buttons, checkboxes with same name) | ||
| if (elements.length > 1) { | ||
| for (let i = 0; i < elements.length; i++) { | ||
| const element = elements[i]; | ||
| if (element.type === 'checkbox') { | ||
| element.checked = value; | ||
| } else if (element.type === 'radio') { | ||
| element.checked = (element.value === value); | ||
| } | ||
| } | ||
| } else { | ||
| const element = elements.length !== undefined ? elements[0] : elements; | ||
|
Comment on lines
+101
to
+139
|
||
| const type = element.type; | ||
|
|
||
| if (type === 'checkbox') { | ||
| element.checked = value; | ||
| } else if (type === 'radio') { | ||
| element.checked = (element.value === value); | ||
| } else if (element.tagName === 'SELECT') { | ||
| if (element.multiple && Array.isArray(value)) { | ||
| for (let i = 0; i < element.options.length; i++) { | ||
| element.options[i].selected = value.includes(element.options[i].value); | ||
| } | ||
| } else { | ||
| element.value = value; | ||
| } | ||
| } else if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') { | ||
| // Always restore - sessionStorage takes precedence | ||
| // If we got this far, the matching logic already determined | ||
| // this is new data that should be restored | ||
| element.value = value; | ||
| } | ||
| } | ||
|
Comment on lines
+124
to
+160
|
||
| } | ||
| } catch (e) { | ||
| console.warn('Failed to restore form data from sessionStorage:', e); | ||
| } | ||
| } | ||
|
|
||
| // Clear saved form data (called after successful submission) | ||
| function clearFormData() { | ||
| try { | ||
| sessionStorage.removeItem(getStorageKey()); | ||
| } catch (e) { | ||
| console.warn('Failed to clear form data from sessionStorage:', e); | ||
| } | ||
| } | ||
|
|
||
| // Initialize auto-save functionality | ||
| function init() { | ||
| const form = document.getElementById('cfp-submission-form'); | ||
| if (!form) return; | ||
|
|
||
| // Restore form data on page load | ||
| restoreFormData(); | ||
|
|
||
| // Create debounced save function (save 500ms after user stops typing) | ||
| const debouncedSave = debounce(saveFormData, 500); | ||
|
|
||
| // Attach event listeners to form elements | ||
| form.addEventListener('input', debouncedSave); | ||
| form.addEventListener('change', debouncedSave); | ||
|
|
||
| // Save before page unload (browser back button, refresh, close tab, etc.) | ||
| // This ensures data is preserved even when using browser navigation | ||
| window.addEventListener('beforeunload', saveFormData); | ||
|
|
||
| // Note: We don't clear sessionStorage on form submit because: | ||
| // 1. We can't reliably detect if submission succeeded (might have validation errors) | ||
| // 2. The restore logic already handles this by clearing sessionStorage when | ||
| // it detects the form has server data (successful previous submission) | ||
| } | ||
|
|
||
| // Initialize when DOM is ready | ||
| if (document.readyState === 'loading') { | ||
| document.addEventListener('DOMContentLoaded', init); | ||
| } else { | ||
| init(); | ||
| } | ||
| })(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use different quote style when nesting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, thanks