Enhance navigation after save by improving URL handling and fallback mechanisms for question transitions
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user