Enhance navigation after save by improving URL handling and fallback mechanisms for question transitions

This commit is contained in:
Ross
2026-01-05 14:04:30 +00:00
parent 3928df6d75
commit 20a06323d5
2 changed files with 82 additions and 75 deletions
+71 -71
View File
@@ -36,7 +36,7 @@
<div>
<p><span id="question-stem">{{ question.stem|safe }}</span></p>
</div>
<div id="question-fragment"
hx-trigger="load"
@@ -85,20 +85,20 @@
.exam-take-container { max-width: 980px; margin: 0 auto; padding: 1rem; }
.no-select { user-select: none; }
.flex-container { display: flex; align-items: center; gap: 0.75rem; }
.flex-container { display: flex; align-items: center; gap: 0.75rem; }
/* fieldWrapper is used around each label + input; ensure it stretches the full width */
.fieldWrapper { width: 100%; display: flex; align-items: center; gap: 0.75rem; }
.fieldWrapper { width: 100%; display: flex; align-items: center; gap: 0.75rem; }
/* We keep two main columns inside each fieldWrapper:
- the label (question-text) which should take the bulk of space and sit to the left on wide screens
- the input container (.flex-1) which holds the input and a post-input label; the post-input should float right
*/
.flex-8 { flex: 8; }
.flex-1 { flex: 1; display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
.flex-8 { flex: 8; }
.flex-1 { flex: 1; display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
/* Left-align label text on wide screens so the question text appears on the left, input between, and the
post-input label floats to the right. Padding keeps visual separation. */
.question-text { margin: 0; text-align: left; padding-right: 0.5rem; }
.question-text { margin: 0; text-align: left; padding-right: 0.5rem; }
/* Answer items styled as cards */
.physics-answer-list { list-style: none; padding: 0; margin: 0; }
@@ -118,8 +118,8 @@
.question-text { margin: 0; }
/* Post-input badge (shows True/False text next to inputs) */
.postinput { display: inline-block; min-width: 3rem; text-align: right; color: var(--muted); }
.physics-answer-list input { margin: 0 0.25rem 0 0; }
.postinput { display: inline-block; min-width: 3rem; text-align: right; color: var(--muted); }
.physics-answer-list input { margin: 0 0.25rem 0 0; }
/* show a compact post-label on the right of the input container */
.physics-answer-list input:checked + .postinput::after {
content: 'True'; color: var(--success); font-weight: 600; margin-left: 0.4rem;
@@ -177,15 +177,15 @@
.mt-2 .save.btn.btn-default { background: var(--feedback-bg); border: 1px solid var(--card-border); color: var(--text); }
/* Overview button - make it stand out using the primary color */
#overview-button { background: var(--primary); border-color: var(--primary); color: #fff; }
#overview-button:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
#overview-button { background: var(--primary); border-color: var(--primary); color: #fff; }
#overview-button:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
/* Previous / Next button colors */
.mt-2 button[name="previous"] { background: var(--info); border-color: var(--info); color: #fff; }
.mt-2 button[name="previous"]:hover { filter: brightness(0.95); box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
.mt-2 button[name="previous"] { background: var(--info); border-color: var(--info); color: #fff; }
.mt-2 button[name="previous"]:hover { filter: brightness(0.95); box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
.mt-2 button[name="next"] { background: var(--info); border-color: var(--info); color: #fff; }
.mt-2 button[name="next"]:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
.mt-2 button[name="next"] { background: var(--info); border-color: var(--info); color: #fff; }
.mt-2 button[name="next"]:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
/* Small helper: make labels wrap nicely on small screens */
@media (max-width: 576px) {
@@ -201,77 +201,77 @@
{% endblock %}
{% block js %}
<script>
<script>
/* HTMX swap helpers: preserve scroll and container height to avoid page jumping
Listens for htmx:beforeSwap and htmx:afterSwap on the question fragment container.
*/
(function(){
function getTarget(evt){
return (evt.detail && (evt.detail.target || evt.detail.elt)) || evt.target;
}
(function(){
function getTarget(evt){
return (evt.detail && (evt.detail.target || evt.detail.elt)) || evt.target;
}
document.addEventListener('htmx:beforeSwap', function(evt){
try{
var target = getTarget(evt);
if(!target || target.id !== 'question-fragment') return;
document.addEventListener('htmx:beforeSwap', function(evt){
try{
var target = getTarget(evt);
if(!target || target.id !== 'question-fragment') return;
// save scroll position
target.__savedScrollY = window.scrollY || window.pageYOffset || 0;
target.__savedScrollY = window.scrollY || window.pageYOffset || 0;
// preserve current height to avoid layout shift while fragment is swapping
target.style.minHeight = target.offsetHeight + 'px';
}catch(e){ console && console.error && console.error(e); }
});
target.style.minHeight = target.offsetHeight + 'px';
}catch(e){ console && console.error && console.error(e); }
});
document.addEventListener('htmx:afterSwap', function(evt){
try{
var target = getTarget(evt);
if(!target || target.id !== 'question-fragment') return;
var y = target.__savedScrollY;
if(typeof y === 'number') window.scrollTo(0, y);
document.addEventListener('htmx:afterSwap', function(evt){
try{
var target = getTarget(evt);
if(!target || target.id !== 'question-fragment') return;
var y = target.__savedScrollY;
if(typeof y === 'number') window.scrollTo(0, y);
// remove the minHeight after a short delay to allow content to layout
setTimeout(function(){ target.style.minHeight = ''; }, 60);
setTimeout(function(){ target.style.minHeight = ''; }, 60);
// notify fragment-loaded so fragment init code can listen if required
target.dispatchEvent(new CustomEvent('fragment:loaded', { bubbles: true }));
}catch(e){ console && console.error && console.error(e); }
});
})();
</script>
<script>
target.dispatchEvent(new CustomEvent('fragment:loaded', { bubbles: true }));
}catch(e){ console && console.error && console.error(e); }
});
})();
</script>
<script>
// Global listener for server-triggered save confirmations.
// The view sets an `HX-Trigger` header named `saved` when an HTMX save occurs.
document.addEventListener('saved', function(evt){
try {
document.addEventListener('saved', function(evt){
try {
// Prefer toastr if available
if (typeof toastr !== 'undefined' && toastr && typeof toastr.success === 'function') {
toastr.success('Saved');
return;
}
if (typeof toastr !== 'undefined' && toastr && typeof toastr.success === 'function') {
toastr.success('Saved');
return;
}
// Lightweight fallback toast
const id = 'htmx-save-toast';
let el = document.getElementById(id);
if (!el) {
el = document.createElement('div');
el.id = id;
el.style.position = 'fixed';
el.style.top = '1rem';
el.style.right = '1rem';
el.style.zIndex = 2000;
el.style.padding = '0.6rem 0.9rem';
el.style.background = 'rgba(40,167,69,0.95)';
el.style.color = '#fff';
el.style.borderRadius = '0.4rem';
el.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
el.style.fontWeight = '600';
el.style.opacity = '0';
el.style.transition = 'opacity 220ms ease-in-out';
document.body.appendChild(el);
}
el.textContent = 'Saved';
const id = 'htmx-save-toast';
let el = document.getElementById(id);
if (!el) {
el = document.createElement('div');
el.id = id;
el.style.position = 'fixed';
el.style.top = '1rem';
el.style.right = '1rem';
el.style.zIndex = 2000;
el.style.padding = '0.6rem 0.9rem';
el.style.background = 'rgba(40,167,69,0.95)';
el.style.color = '#fff';
el.style.borderRadius = '0.4rem';
el.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
el.style.fontWeight = '600';
el.style.opacity = '0';
el.style.transition = 'opacity 220ms ease-in-out';
document.body.appendChild(el);
}
el.textContent = 'Saved';
// show
requestAnimationFrame(function(){ el.style.opacity = '1'; });
requestAnimationFrame(function(){ el.style.opacity = '1'; });
// hide after 1.6s
setTimeout(function(){ el.style.opacity = '0'; }, 1600);
} catch (e) { console && console.error && console.error(e); }
});
</script>
setTimeout(function(){ el.style.opacity = '0'; }, 1600);
} catch (e) { console && console.error && console.error(e); }
});
</script>
{% endblock %}
@@ -170,8 +170,15 @@
if(hxTrigger && hxTrigger.indexOf('saved') !== -1){
handled = true;
if(window.htmx && typeof htmx.off === 'function') htmx.off('htmx:afterRequest', htmxHandler);
document.getElementById('goto-button').value = destIndex;
$('#goto-button').click();
try{
const pageUrl = `{% if cid %}{% url 'physics:exam_take' pk=exam.pk sk=0 cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_user' pk=exam.pk sk=0 %}{% endif %}`.replace('/0/', '/' + destIndex + '/');
window.location = pageUrl;
}catch(e){
if(document.getElementById('goto-button')){
document.getElementById('goto-button').value = destIndex;
$('#goto-button').click();
}
}
}
}catch(e){/* ignore */}
}
@@ -181,13 +188,13 @@
}
// As a fallback listen for the custom 'saved' event
const onSavedFallback = function(){ if(handled) return; handled = true; document.removeEventListener('saved', onSavedFallback); document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); };
const onSavedFallback = function(){ if(handled) return; handled = true; document.removeEventListener('saved', onSavedFallback); try{ const pageUrl = `{% if cid %}{% url 'physics:exam_take' pk=exam.pk sk=0 cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_user' pk=exam.pk sk=0 %}{% endif %}`.replace('/0/', '/' + destIndex + '/'); window.location = pageUrl; }catch(e){ if(document.getElementById('goto-button')){ document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); } } };
document.addEventListener('saved', onSavedFallback);
tmp.click();
// final timeout fallback
setTimeout(function(){ if(!handled){ try{ document.removeEventListener('saved', onSavedFallback); if(window.htmx && typeof htmx.off === 'function') htmx.off('htmx:afterRequest', htmxHandler); document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); }catch(e){} } }, 1500);
setTimeout(function(){ if(!handled){ try{ document.removeEventListener('saved', onSavedFallback); if(window.htmx && typeof htmx.off === 'function') htmx.off('htmx:afterRequest', htmxHandler); const pageUrl = `{% if cid %}{% url 'physics:exam_take' pk=exam.pk sk=0 cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_user' pk=exam.pk sk=0 %}{% endif %}`.replace('/0/', '/' + destIndex + '/'); window.location = pageUrl; }catch(e){ try{ if(document.getElementById('goto-button')){ document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); } }catch(e){} } } }, 1500);
return;
}
}